#ifndef AAB5E4D2_A05B_4F2F_B76A_406A5A569D55
#define AAB5E4D2_A05B_4F2F_B76A_406A5A569D55

#include "mana_api.hpp"
#include "mana_arena.h"
#include <boost/dll.hpp>
#include <boost/program_options/parsers.hpp>
#include <cassert>
#include <chrono>
#include <mesytec-mnode/mnode_cpp_types.h>
#include <mesytec-mnode/mnode_string.hpp>
#include <mesytec-mvlc/cpp_compat.h>
#include <mesytec-mvlc/util/data_filter.h>
#include <nlohmann/json.hpp>

namespace mesytec::mnode::mana
{

inline bool is_null(const mana_offset_ptr_t &ptr) { return ptr.offset == 1; }

namespace detail
{

inline void set(mana_offset_ptr_t &ptr, void *p)
{
    assert(p != reinterpret_cast<u8 *>(&ptr) + 1);

    if (p)
    {
        ptr.offset = reinterpret_cast<u8 *>(p) - reinterpret_cast<u8 *>(&ptr);
        // spdlog::info("detail::set: &ptr={}, p={}, offset={}", fmt::ptr(&ptr), fmt::ptr(p),
        // ptr.offset);
    }
    else
    {
        ptr.offset = 1;
    }
}

inline void set(mana_offset_ptr_t &ptr, mana_data_type_t data_type, void *p)
{
    ptr.data_type = data_type;
    detail::set(ptr, p);
}

inline void *get(mana_offset_ptr_t &ptr)
{
    return is_null(ptr) ? nullptr : reinterpret_cast<u8 *>(&ptr) + ptr.offset;
}

template <typename T> T *get(mana_offset_ptr_t &ptr, mana_data_type_t expected)
{
    if (ptr.data_type != expected)
        return nullptr;

    return reinterpret_cast<T *>(get(ptr));
}

} // namespace detail

template <typename T> T *get(mana_offset_ptr_t &ptr); // to catch unsupported types

template <> u32 *get(mana_offset_ptr_t &ptr) { return detail::get<u32>(ptr, mana_uint32); }
template <> u64 *get(mana_offset_ptr_t &ptr) { return detail::get<u64>(ptr, mana_uint64); }
template <> s8 *get(mana_offset_ptr_t &ptr) { return detail::get<s8>(ptr, mana_sint8); }
template <> char *get(mana_offset_ptr_t &ptr) { return detail::get<char>(ptr, mana_sint8); }
template <> float *get(mana_offset_ptr_t &ptr) { return detail::get<float>(ptr, mana_float); }
template <> double *get(mana_offset_ptr_t &ptr) { return detail::get<double>(ptr, mana_double); }

// To make 'set(ptr, nullptr)' work.
inline void set(mana_offset_ptr_t &ptr, std::nullptr_t)
{
    detail::set(ptr, ptr.data_type, nullptr);
}

inline void set(mana_offset_ptr_t &ptr, u8 *p) { detail::set(ptr, mana_uint8, p); }
inline void set(mana_offset_ptr_t &ptr, s8 *p) { detail::set(ptr, mana_sint8, p); }
inline void set(mana_offset_ptr_t &ptr, char *p) { detail::set(ptr, mana_sint8, p); }
inline void set(mana_offset_ptr_t &ptr, u16 *p) { detail::set(ptr, mana_uint16, p); }
inline void set(mana_offset_ptr_t &ptr, s16 *p) { detail::set(ptr, mana_sint16, p); }
inline void set(mana_offset_ptr_t &ptr, u32 *p) { detail::set(ptr, mana_uint32, p); }
inline void set(mana_offset_ptr_t &ptr, s32 *p) { detail::set(ptr, mana_sint32, p); }
inline void set(mana_offset_ptr_t &ptr, u64 *p) { detail::set(ptr, mana_uint64, p); }
inline void set(mana_offset_ptr_t &ptr, s64 *p) { detail::set(ptr, mana_sint64, p); }
inline void set(mana_offset_ptr_t &ptr, float *p) { detail::set(ptr, mana_float, p); }
inline void set(mana_offset_ptr_t &ptr, double *p) { detail::set(ptr, mana_double, p); }

template <typename T>
T *push_offset_array(mana::Arena &arena, mana_offset_array_t &dest, size_t size)
{
    T *ptr = arena.push_t<T>(size);
    mana::set(dest.ptr, mana_custom, ptr);
    dest.size_bytes = size * sizeof(T);
    return ptr;
}

template <typename T>
T *push_typed_offset_array(mana::Arena &arena, mana_offset_array_t &dest, size_t size)
{
    T *ptr = arena.push_t<T>(size);
    set(dest.ptr, ptr);
    dest.size_bytes = size * sizeof(T);
    return ptr;
}

template <typename T> size_t element_count(mana_offset_array_t &array)
{
    return array.size_bytes / sizeof(T);
}

template <typename T> mvlc::util::span<T> get_span(mana_offset_array_t &array)
{
    auto ptr = get<T>(array.ptr);
    auto size = element_count<T>(array);
    return {ptr, size};
}

inline nlohmann::json make_array_descriptor(const std::string &name, mana_data_type_t data_type,
                                            size_t size, size_t bits = 0)
{
    nlohmann::json j;
    j["name"] = name;
    j["data_type"] = data_type;
    j["size"] = size;
    j["bits"] = bits;
    if (bits != 0)
    {
        j["lower_limit"] = 0u;
        j["upper_limit"] = 1lu << bits;
    }
    return j;
}

inline nlohmann::json make_array_descriptor(const std::string &name, const std::string &bit_filter)
{
    auto f = mvlc::util::make_filter_with_caches(bit_filter);
    size_t size = 1;

    if (auto c = mvlc::util::get_cache_entry(f, 'A'))
        size = 1u << c->extractBits;

    size_t bits = 0;

    if (auto c = mvlc::util::get_cache_entry(f, 'D'))
        bits = c->extractBits;

    return make_array_descriptor(name, mana_float, size, bits);
}

class ManaSinkPerfProxy: public IManaSink
{
  public:
    struct Perf
    {
        using clock = std::chrono::high_resolution_clock;
        using time_point = std::chrono::time_point<clock>;
        using duration = std::chrono::microseconds;
        template <typename T> static duration duration_cast(const T &dt)
        {
            return std::chrono::duration_cast<duration>(dt);
        }
        std::vector<size_t> eventHits;
        std::vector<size_t> eventBytes;
        duration dt_beginRun;
        duration dt_endRun;
        std::vector<duration> dt_processEvent;
        time_point t_beginRun;
        time_point t_endRun;
    };

    explicit ManaSinkPerfProxy(IManaSink *sink)
        : sink_(sink)
    {
    }

    const Perf &perf() const { return perf_; }

    void begin_run(const char *descriptor_json) override
    {
        perf_.eventHits.clear();
        perf_.eventBytes.clear();
        perf_.dt_beginRun = perf_.dt_endRun = {};
        perf_.dt_processEvent.clear();
        perf_.t_endRun = {};

        auto t = Perf::clock::now();
        sink_->begin_run(descriptor_json);
        perf_.t_beginRun = Perf::clock::now();
        perf_.dt_beginRun = Perf::duration_cast(Perf::clock::now() - t);
    }

    void end_run(const char *descriptor_json) override
    {
        auto t = Perf::clock::now();
        sink_->end_run(descriptor_json);
        perf_.t_endRun = Perf::clock::now();
        perf_.dt_endRun = Perf::duration_cast(perf_.t_endRun - t);
    }

    void process_event(uint16_t eventIndex, mana_offset_array_t *arrays, size_t arrayCount,
                       size_t totalBytes) override
    {
        size_t size = eventIndex + 1;
        perf_.eventHits.resize(std::max(perf_.eventHits.size(), size));
        perf_.eventBytes.resize(std::max(perf_.eventBytes.size(), size));
        perf_.dt_processEvent.resize(std::max(perf_.dt_processEvent.size(), size));

        auto t = Perf::clock::now();
        sink_->process_event(eventIndex, arrays, arrayCount, totalBytes);
        auto dt = Perf::duration_cast(Perf::clock::now() - t);

        perf_.eventHits[eventIndex]++;
        perf_.eventBytes[eventIndex] += totalBytes;
        perf_.dt_processEvent[eventIndex] += dt;
    }

    void process_system_event(const uint32_t *data, size_t size) override
    {
        sink_->process_system_event(data, size);
    }

  private:
    IManaSink *sink_;
    Perf perf_;
};

inline std::string to_string(const ManaSinkPerfProxy::Perf &perf)
{
    auto totalBytes = std::accumulate(std::begin(perf.eventBytes), std::end(perf.eventBytes),
                                      static_cast<size_t>(0));
    auto totalHits = std::accumulate(std::begin(perf.eventHits), std::end(perf.eventHits),
                                     static_cast<size_t>(0));
    double elapsed_ms =
        std::chrono::duration_cast<std::chrono::milliseconds>(perf.t_endRun - perf.t_beginRun)
            .count();
    double elapsed_s = elapsed_ms / 1000.0;

    double MiB = totalBytes / (1024.0 * 1024);
    double MiB_s = MiB / elapsed_s;
    double hit_s = totalHits / elapsed_s;

    return fmt::format("events={}, bytes={:.2f} MiB, elapsed={:.2f} s, "
                       "event_rate={:.2f} event/s, data_rate={:.2f} MiB/s",
                       totalHits, MiB, elapsed_s, hit_s, MiB_s);
}

#if 0
class ManaBufferingSink: public IManaSink
{
  public:
    using flush_predicate = std::function<bool()>;
    explicit ManaBufferingSink(IManaBufferSink *dest, flush_predicate doFlush)
        : dest_(dest)
        , doFlush_(doFlush)
    {
    }

    void begin_run(const char *descriptor_json) override { dest_->begin_run(descriptor_json); }

    void end_run(const char *descriptor_json) override { dest_->end_run(descriptor_json); }

    void process_event(uint16_t eventIndex, mana_offset_array_t *arrays, size_t arrayCount,
                       size_t totalBytes) override
    {
    }

    void process_system_event(const uint32_t *data, size_t size) override {}

  private:
    IManaBufferSink *dest_;
    flush_predicate doFlush_;
};

class ManaDebufferingSink: public IManaBufferSink
{
  public:
    explicit ManaDebufferingSink(IManaSink *dest)
        : dest_(dest)
    {
    }

    void begin_run(const char *descriptor_json) override { dest_->begin_run(descriptor_json); }

    void end_run(const char *descriptor_json) override { dest_->end_run(descriptor_json); }

    void process_event_buffer(const uint8_t *buffer, size_t size) override {}

    void process_system_event(const uint32_t *data, size_t size) override {}

  private:
    IManaSink *dest_;
};
#endif

struct mana_c_error: public std::exception
{
    mana_status_t status;
    mana_c_error(const mana_status_t &status_)
        : status(status_)
    {
    }
    const char *what() const noexcept override { return status.message; }
};

// wraps a mana_api.h mana_sink_plugin_t instance
struct ManaCSink: public IManaSink
{
    mana_sink_plugin_t plugin_;
    void *context_ = nullptr;

    explicit ManaCSink(mana_sink_plugin_t plugin, int plugin_argc, const char **plugin_argv)
        : plugin_(plugin)
        , context_(plugin_.init(plugin_argc, plugin_argv))
    {
        if (!context_)
            throw std::runtime_error("ManaCSink: plugin init failed");
    }

    ~ManaCSink() override { plugin_.shutdown(context_); }

    void begin_run(const char *descriptor_json) override
    {
        if (auto status = plugin_.begin_run(context_, descriptor_json);
            status.code != mana_status_ok)
        {
            throw mana_c_error(status);
        }
    }

    void end_run(const char *descriptor_json) override
    {
        if (auto status = plugin_.end_run(context_, descriptor_json); status.code != mana_status_ok)
        {
            throw mana_c_error(status);
        }
    }

    void process_event(uint16_t eventIndex, mana_offset_array_t *arrays, size_t arrayCount,
                       size_t totalBytes) override
    {
        plugin_.process_event(context_, eventIndex, arrays, arrayCount, totalBytes);
    }

    void process_system_event(const uint32_t *data, size_t size) override
    {
        plugin_.process_system_event(context_, data, size);
    }
};

struct ObjectPath
{
    std::string path;
    std::string objectName;
    std::vector<std::string> components;

    ObjectPath(const std::string &path, const std::string &sep = "\\.")
        : path(path)
    {
        split_string(path, std::regex(sep), std::back_inserter(components));

        if (!components.empty())
        {
            objectName = components.back();
            components.pop_back();
        }
    }
};

struct PluginWrapper
{
    boost::dll::shared_library pluginHandle;
    std::unique_ptr<mana::IManaPlugin> manaCppPlugin;
    std::unique_ptr<mana::IManaSink> destSink;
};

PluginWrapper load_mana_plugin(const std::string &filename,
                               const std::vector<std::string> &pluginArgs)
{
    PluginWrapper result;
    result.pluginHandle = boost::dll::shared_library(filename, boost::dll::load_mode::rtld_global);

    try
    {
        if (auto entryPoint = result.pluginHandle.get<mana_sink_plugin_t()>("mana_get_sink_plugin"))
        {
            std::vector<const char *> argv;
            for (const auto &arg: pluginArgs)
                argv.push_back(arg.data());
            result.destSink =
                std::make_unique<mana::ManaCSink>(entryPoint(), argv.size(), argv.data());
        }
    }
    catch (const std::exception &e)
    {
        if (auto entryPoint = result.pluginHandle.get<mana::IManaPlugin *()>("mana_get_plugin"))
        {
            result.manaCppPlugin = std::unique_ptr<mana::IManaPlugin>(entryPoint());
            if (!result.manaCppPlugin)
                throw std::runtime_error("plugin {}: mana_get_plugin() returned nullptr");
            result.destSink = result.manaCppPlugin->makeSink();
        }
    }

    return result;
}

PluginWrapper load_mana_plugin(const std::string &filename, const std::string &cmdline)
{
    return load_mana_plugin(filename, boost::program_options::split_unix(cmdline));
}

} // namespace mesytec::mnode::mana

#endif /* AAB5E4D2_A05B_4F2F_B76A_406A5A569D55 */
