#pragma once

#include "ProtobufMessages.hpp"
#include "WorldStateWriter.hpp"
#include "debug/metrics/NetworkMetrics.hpp"
#include "network/EntityIdMap.hpp"
#include "network/PlayerRollback.hpp"
#include "network/ServerConnection.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "network/EntityPositionInterpolator.hpp"
#include "world/World.hpp"

namespace tw::net {


/**
 * Handles replication messages from server.
 */
class ReplicatorClient {
public:
    ReplicatorClient(
            ProtobufMessages* messages,
            EntityIdMap* entity_id_map,
            dbg::NetworkMetrics *network_metrics,
            JoltPhysicsWorld *physics_world,
            World *world,
            net::ServerConnection* server_connection,
            EntityPositionInterpolator* entity_writer
    );

    /**
     * Sets player controlled entity. The behaviour for this entity will be
     * little different.
     */
    void set_player_entity(entt::entity entity);

    /**
     * Takes the input driving `frame_idx`, keeping it for the answer to that
     * frame and putting it on the wire.
     */
    void set_input(uint32_t frame_idx, glm::vec3 input);

    /**
     * Takes where the player ended up on `frame_idx`, which should represent
     * the prediction taken.
     */
    void record_prediction(uint32_t frame_idx);

    void handle_snapshot(uint32_t current_frame_idx, serial::WorldStateReader& reader);

private:
    /**
     * Records how long the answer to `frame_idx` took to arrive, ignoring
     * frames that were already measured or are too old to still have a send
     * time.
     */
    void measure_response_time(uint32_t current_frame_idx, uint32_t snapshot_frame_idx);

    /**
     * Places the player at an authoritative position outright, clearing the
     * predicted state that led there. Used for the first position the server
     * sends, which the local simulation has no history to reconcile against.
     */
    void snap_player_to(uint32_t frame_idx, entt::entity entity, glm::vec3 position);

    void handle_snapshot_entity(
        uint32_t current_frame_idx,
        uint32_t record_frame_idx,
        serial::EntityRecord& record
    );

    void update();

    glm::vec3 render_position() const {
        return m_render_position;
    }

private:
    using Clock = std::chrono::steady_clock;

    static int64_t now_ms();

    ProtobufMessages *m_messages;
    EntityIdMap* m_entity_id_map;
    dbg::NetworkMetrics *m_network_metrics;
    net::PlayerRollback m_rollback;
    World *m_world;
    net::ServerConnection* m_server_connection;

    std::optional<entt::entity> m_player_entity;

    net::EntityPositionInterpolator* m_entity_interpolator;

    /**
     * Whether the server has placed the player at least once. Entities are
     * created before their position arrives, so the body starts at the origin
     * and has to be moved once the first position shows up.
     */
    bool m_player_position_initialized = false;

    glm::vec3 m_render_position{0.0f};

    /**
     * When each input was sent, indexed by its frame. Holds the most recent
     * INPUT_SEND_TIME_COUNT frames; an answer that takes longer than that goes
     * unmeasured.
     */
    static constexpr size_t INPUT_SEND_TIME_COUNT = 256;

    std::vector<Clock::time_point> m_input_send_times;
    uint32_t                       m_last_measured_frame = 0;
};

}
