#pragma once

#include "MayaFlux/Nexus/Pheme/InfluenceContext.hpp"
#include "MayaFlux/Nexus/Pheme/Sinks.hpp"

namespace MayaFlux::Buffers {
class VKBuffer;
}

namespace MayaFlux::Nexus {

/**
 * @class Emitter
 * @brief Object that acts on existing MayaFlux objects when committed.
 *
 * Constructed with only an influence function. Position is optional: call
 * @c set_position before registering with @c Fabric if spatial indexing is
 * required. Entities without a position are committed normally but are not
 * inserted into the spatial index.
 *
 * The id is assigned by @c Fabric::wire and is stable for the object's
 * lifetime. It is zero until the object has been registered.
 */
class MAYAFLUX_API Emitter {
public:
    using InfluenceFn = std::function<void(const InfluenceContext&)>;

    /**
     * @brief Construct with an influence function.
     * @param fn Called on every commit with the current context.
     */
    explicit Emitter(InfluenceFn fn)
        : m_fn(std::move(fn))
    {
    }

    /**
     * @brief Construct with a named influence function.
     * @param fn_name Identifier used for state encoding.
     * @param fn      Called on every commit with the current context.
     */
    Emitter(std::string fn_name, InfluenceFn fn)
        : m_fn_name(std::move(fn_name))
        , m_fn(std::move(fn))
    {
    }

    /** @brief Identifier assigned to the influence function, empty if anonymous. */
    [[nodiscard]] const std::string& fn_name() const { return m_fn_name; }

    /** @brief Set or replace the influence function's identifier. */
    void set_fn_name(std::string name) { m_fn_name = std::move(name); }

    /** @brief The influence function itself. */
    [[nodiscard]] const InfluenceFn& fn() const { return m_fn; }

    /**
     * @brief Return the current position, if set.
     */
    [[nodiscard]] const std::optional<glm::vec3>& position() const { return m_position; }

    /**
     * @brief Set the position, enabling spatial indexing for this object.
     * @param p World-space coordinates.
     */
    void set_position(const glm::vec3& p) { m_position = p; }

    /**
     * @brief Clear the position, removing this object from spatial queries.
     */
    void clear_position() { m_position.reset(); }

    /**
     * @brief Return the stable object id assigned by Fabric.
     */
    [[nodiscard]] uint32_t id() const { return m_id; }

    // =========================================================================
    // Output sinks
    // =========================================================================

    /** @brief Register an audio output on @p channel. */
    void sink_audio(Buffers::BufferManager& mgr, uint32_t channel)
    {
        add_audio_sink(m_audio_sinks, mgr, channel);
    }

    /** @brief Register an audio output on @p channel with a producer function. */
    void sink_audio(Buffers::BufferManager& mgr, uint32_t channel,
        std::function<Kakshya::DataVariant(const InfluenceContext&)> fn,
        std::string fn_name = "")
    {
        add_audio_sink(m_audio_sinks, mgr, channel, std::move(fn), std::move(fn_name));
    }

    /** @brief Unregister the audio sink on @p channel. */
    void remove_audio_sink(Buffers::BufferManager& mgr, uint32_t channel)
    {
        Nexus::remove_audio_sink(m_audio_sinks, mgr, channel);
    }

    /** @brief Register a render output targeting @p window. */
    void render(Buffers::BufferManager& mgr, const Portal::Graphics::RenderConfig& config)
    {
        add_render_sink(m_render_sinks, mgr, config, {}, "", m_position);
    }

    /** @brief Register a render output targeting @p window with a producer function. */
    void render(Buffers::BufferManager& mgr, const Portal::Graphics::RenderConfig& config,
        std::string fn_name, RenderFn fn)
    {
        add_render_sink(m_render_sinks, mgr, config, std::move(fn), std::move(fn_name), m_position);
    }

    [[nodiscard]] const std::vector<AudioSink>& audio_sinks() const { return m_audio_sinks; }
    [[nodiscard]] const std::vector<RenderSink>& render_sinks() const { return m_render_sinks; }

    /* @brief Return the render processor for the sink targeting @p window, or nullptr if not found. */
    std::shared_ptr<Buffers::RenderProcessor> get_render_processor(
        const std::shared_ptr<Core::Window>& window) const
    {
        auto it = std::ranges::find_if(m_render_sinks,
            [&window](const RenderSink& s) { return s.window == window; });
        return it != m_render_sinks.end() ? it->renderer : nullptr;
    }

    /** @brief Unregister the render sink targeting @p window. */
    void remove_render(Buffers::BufferManager& mgr, const std::shared_ptr<Core::Window>& window)
    {
        remove_render_sink(m_render_sinks, mgr, window);
    }

    /** @brief Push @p samples to all registered audio sinks. */
    void set_audio_data(std::span<const double> samples)
    {
        push_audio_data(m_audio_sinks, samples);
    }

    /**
     * @brief Set pre-packed interleaved vertex bytes to all registered render sinks.
     *
     * data must point to N contiguous 60-byte Vertex records.
     * byte_count must be a multiple of 60.
     */
    void set_vertices(const void* data, size_t byte_count);

    /**
     * @brief Set typed vertex data to all registered render sinks.
     * @tparam T One of Kakshya::PointVertex, Kakshya::LineVertex, Kakshya::MeshVertex, Kakshya::Vertex.
     */
    template <typename T>
    void set_vertices(std::span<const T> vertices)
    {
        static_assert(sizeof(T) == 60, "set_vertices: T must be a 60-byte vertex type");
        set_vertices(vertices.data(), vertices.size_bytes());
    }

    /** @brief Set the intensity, a general-purpose parameter for influence functions. */
    void set_intensity(float i) { m_intensity = i; }

    /** @brief Get the current intensity. */
    [[nodiscard]] float intensity() const { return m_intensity; }

    /** @brief Set the radius, a general-purpose parameter for influence functions. */
    void set_radius(float r) { m_radius = r; }

    /** @brief Get the current radius. */
    [[nodiscard]] float radius() const { return m_radius; }

    /** @brief Set the color, a general-purpose parameter for influence functions. */
    void set_color(const glm::vec3& c) { m_color = c; }

    /** @brief Clear the color, resetting it to an unset state. */
    void clear_color() { m_color.reset(); }

    /** @brief Get the current color, if set. */
    [[nodiscard]] const std::optional<glm::vec3>& color() const { return m_color; }

    /** @brief Set the size, a general-purpose parameter for influence functions. */
    void set_size(float s) { m_size = s; }

    /** @brief Clear the size, resetting it to an unset state. */
    void clear_size() { m_size.reset(); }

    /** @brief Get the current size, if set. */
    [[nodiscard]] const std::optional<float>& size() const { return m_size; }

    /**
     * @brief Set the render processor to target for GPU-side influence delivery.
     *
     * Creates a UBO matching the InfluenceUBO layout, registers a binding
     * named "u_influence" at set=1 binding=0 on the target processor,
     * and binds the UBO. On each subsequent invoke(), the context fields
     * are packed into the UBO automatically.
     *
     * @param proc Target render processor. Must outlive this Emitter or
     *             be cleared via clear_influence_target() first.
     * @param set   Descriptor set index for the UBO binding. Default is 1.
     * @param binding Descriptor binding index for the UBO. Default is 0.
     */
    void set_influence_target(std::shared_ptr<Buffers::RenderProcessor> proc,
        uint32_t set = 1, uint32_t binding = 0);

    /**
     * @brief Disconnect from the current influence target.
     *
     * Unbinds the "u_influence" descriptor from the target processor
     * and releases the UBO.
     */
    void clear_influence_target();

    /**
     * @brief Return the current influence target, if set.
     */
    [[nodiscard]] std::weak_ptr<Buffers::RenderProcessor> influence_target() const
    {
        return m_influence_target;
    }

    /**
     * @brief Invoke the influence function with the supplied context.
     * @param ctx Populated context for this commit.
     */
    void invoke(const InfluenceContext& ctx) const
    {
        if (m_fn) {
            m_fn(ctx);
        }
        dispatch_audio_sinks(m_audio_sinks, ctx);
        dispatch_render_sinks(m_render_sinks, ctx);
        if (m_influence_ubo)
            upload_influence_ubo(ctx);
    }

private:
    std::optional<glm::vec3> m_position;
    std::optional<glm::vec3> m_color;
    std::optional<float> m_size;
    float m_intensity { 1.0F };
    float m_radius { 1.0F };

    std::shared_ptr<Buffers::RenderProcessor> m_influence_target;
    std::shared_ptr<Buffers::VKBuffer> m_influence_ubo;

    InfluenceFn m_fn;
    uint32_t m_id {};
    std::string m_fn_name;

    mutable std::vector<AudioSink> m_audio_sinks;
    mutable std::vector<RenderSink> m_render_sinks;

    void upload_influence_ubo(const InfluenceContext& ctx) const;

    friend class Fabric;
};

} // namespace MayaFlux::Nexus
