#pragma once

#include <entt/entt.hpp>
#include <glm/gtx/io.hpp>

#include "ProtobufMessages.hpp"
#include "debug/metrics/NetworkMetrics.hpp"
#include "entt/entity/fwd.hpp"
#include "io/InputState.hpp"
#include "metrics/HistoryBufferExporter.hpp"
#include "runtime/LockStep.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/World.hpp"
#include "draw/WorldRenderer.hpp"
#include "world/ThirdPersonPlayerController.hpp"
#include "network/EntityPositionInterpolator.hpp"
#include "network/PlayerReconciler.hpp"

namespace tw::net {
class ServerConnection;
}

namespace tw {

/**
 * Multiplayer player controller. Requires address of a server to work.
 */
class ClientWorldController {
    /**
     * Inputs
     */
    const io::InputManager* m_input_manager;

    /**
     * Pointers to the world
     */
    World* m_world;
    drw::WorldRenderer* m_world_renderer;
    JoltPhysicsWorld* m_physics_world;

    std::optional<entt::entity> m_player_entity;
    std::optional<uint32_t> m_controlled_server_id;
    ThirdPersonPlayerController m_player_controller;

    net::ServerConnection* m_connection;
    ProtobufMessages       m_messages;

    dbg::NetworkMetrics* m_network_metrics;

    LockStep m_tick_step;

    uint32_t m_frame_idx = 1;
    entt::entity m_entity_id;

    glm::vec3 m_input;

    std::optional<drw::Mesh> m_mesh;

    using Clock = std::chrono::steady_clock;

    void try_bind_player_entity();

    /**
     * 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(entt::entity entity, glm::vec3 position);

    /**
     * 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;

    /**
     * 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;

    /**
     * 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 frame_idx);

    HistoryBufferExporter<long, glm::vec3> m_position_history_exporter;

    net::EntityPositionInterpolator m_entity_interpolator;

    net::PlayerReconciler m_reconciler;

    /**
     * Visual smoothing for render-rate interpolation between 20 Hz ticks.
     */
    glm::vec3 m_render_prev_position{0.0f};
    glm::vec3 m_render_curr_position{0.0f};
    double m_tick_accumulator = 0.0;

    /**
     * Visual error from reconciliation corrections, decays over time.
     */
    glm::vec3 m_visual_error{0.0f};
    static constexpr double kVisualErrorDecayRate = 12.0;

    /**
     * Mapping from the server entity_id to local entity_id
     * Server might have the same entity under different name
     * TODO: Figure out if entt supports custUntitledom IDs to sync them
     */
    std::unordered_map<uint32_t, entt::entity> m_entity_mapping;

    entt::entity create_entity(const std::string& name, glm::vec3 position);

    std::optional<entt::entity> map_from_server_entity(int id);

    void map_server_entity(int server_id, entt::entity local_id);

    void apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count);



    /**
     * Exports entity history to CSV file for analysis
     */
    void export_entity_history();

public:
    ClientWorldController(
            const io::InputManager* inputs,
            World* world,
            JoltPhysicsWorld* physics_world,
            drw::WorldRenderer* world_renderer,
            net::ServerConnection* connection,
            dbg::NetworkMetrics* network_metrics
    );

    ~ClientWorldController();

    void update(double delta_time);
};

}
