#include "ClientWorldController.hpp"

#include <glm/glm.hpp>
#include <chrono>
#include <spdlog/spdlog.h>
#include <span>

#include "network/ServerConnection.hpp"

#include "Entity.pb.h"
#include "Login.pb.h"
#include "WorldState.pb.h"
#include "PlayerMove.pb.h"

#include "entt/entity/entity.hpp"
#include "entt/entity/fwd.hpp"
#include "messages/PlayerMoveMessage.hpp"
#include "metrics/HistoryBuffer.hpp"
#include "world/CharacterBody.hpp"
#include "world/CharacterController.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/WorldEntity.hpp"
#include "tw/serial/WorldStateWriter.hpp"
#include "network/EntityInterpolation.hpp"

namespace tw {

entt::entity
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
    const auto entity = m_world->registry().create();

    if(!m_mesh.has_value()) {
        m_mesh = m_world_renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
    }
    spdlog::info("Creating entity {}", name);

    m_world->registry()
        .emplace<Transform>(entity,
                Transform(position));

    m_world->registry()
        .emplace<tw::WorldEntity>(entity,
                tw::WorldEntity(name, (uint32_t)entity));

    m_world->registry()
        .emplace<tw::drw::Mesh>(entity,
                m_mesh.value());

    return entity;
}

// void ClientWorldController::apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count) {
//     for(int i = 0; i < count; i++) {
//         const mmo::EntityPosition* const position = positions[i];
//         std::optional<entt::entity> entity = m_entity_id_map.get_local(position->id());
//
//         if(!entity.has_value()) {
//             spdlog::warn("Received position update for unknown entity {}", position->id());
//             continue;
//         }
//
//         glm::vec3 p = {position->x(), position->y(), position->z()};
//         m_entity_interpolator.add_position_for_entity(entity.value(), p);
//
//         EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
//
//         if(history != nullptr) {
//             auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
//             history->set(millis, p);
//         }
//     }
// }

void ClientWorldController::spawn_entity(const std::string& name, uint32_t server_id) {
    auto entity = create_entity(name, glm::vec3());
    spdlog::info("Spawning entity {}", server_id);

    m_entity_id_map.register_server_to_local(server_id, entity);

    if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == server_id) {
        try_bind_player_entity();
    } else {
        m_interpolator.register_entity(entity);
    }
}

ClientWorldController::ClientWorldController(
        const io::InputManager* inputs,
        World* world,
        JoltPhysicsWorld* physics_world,
        drw::WorldRenderer* world_renderer,
        net::ServerConnection* connection,
        dbg::NetworkMetrics* network_metrics
) :
    m_input_manager(inputs),
    m_world(world),
    m_physics_world(physics_world),
    m_world_renderer(world_renderer),
    m_player_controller(&world_renderer->camera(), glm::vec3()),
    m_connection(connection),
    m_messages(connection->endpoint()),
    m_network_metrics(network_metrics),
    m_input_update_tick_step(20),
    m_interpolator(&m_world->registry(), 300),
    m_replicator_client(&m_messages, &m_entity_id_map, m_network_metrics, m_physics_world, m_world, m_connection, &m_interpolator)
{
    m_messages.set_handler<mmo::LoginResponse>(
        [this](msg::PeerId, const mmo::LoginResponse& mesg) {
            spdlog::info("Logged in!");
        });

    m_messages.set_handler<mmo::SetControlledEntity>(
        [this](msg::PeerId, const mmo::SetControlledEntity& mesg) {
            spdlog::info("Setting controlled entity from server id {}", mesg.entity_id());
            m_controlled_server_id = mesg.entity_id();
            try_bind_player_entity();
        });

    m_connection->endpoint()->set_handler(Message<mmo::WorldStateMessage>::value,
        [this](msg::PeerId, std::span<const std::byte> data) {

            serial::WorldStateReader reader(data);

            m_replicator_client.handle_snapshot(m_network_frame_idx, reader);
        });

    m_messages.set_handler<mmo::EntitySpawnMessage>(
        [this](msg::PeerId, const mmo::EntitySpawnMessage& mesg) {
            for(auto& spawn : mesg.spawns()) {
                spawn_entity(spawn.name(), spawn.entity_id());
            }
        });
}

ClientWorldController::~ClientWorldController() { }

void ClientWorldController::try_bind_player_entity() {
    if(!m_controlled_server_id.has_value()) {
        return;
    }

    auto local_entity = m_entity_id_map.get_local(m_controlled_server_id.value());
    if(!local_entity.has_value()) {
        return;
    }

    if(m_player_entity.has_value() && m_player_entity.value() == local_entity.value()) {
        return;
    }

    entt::entity entity = local_entity.value();
    spdlog::info("Binding player entity");

    m_player_entity = entity;
    m_replicator_client.set_player_entity(entity);

    Transform* transform = m_world->registry().try_get<Transform>(entity);
    glm::vec3 position = transform ? transform->position() : glm::vec3(0.0f);

    m_world->registry().emplace<CharacterController>(entity, 20.0f);
    m_world->registry().emplace<CharacterBody>(entity, m_physics_world->create_character(
        new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
        position
    ));

    if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
        m_world->registry().remove<net::EntityPositionInterpolation>(entity);
    }
}




void ClientWorldController::update_network() {
    auto network_start = Clock::now();
    // collects incoming messages
    m_connection->update();
    m_network_metrics->record_update_time(Clock::now() - network_start);

    m_network_metrics->sample({
        .bytes_sent        = m_connection->endpoint()->bytes_sent(),
        .bytes_received    = m_connection->endpoint()->bytes_received(),
        .messages_sent     = m_connection->endpoint()->messages_sent(),
        .messages_received = m_connection->endpoint()->messages_received()
    });
   



    // m_world->registry().view<Transform>()
    //     .each([&](const auto e, Transform& t) {
    //             m_position_history_exporter.write((uint32_t)e,
    //                     std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count(), t.position());
    //             });

}

void ClientWorldController::update(double delta_time) {
    // collects messages from network for processing
    update_network();

    m_player_controller.update(m_input_manager, delta_time);

    // collect input every tick of the input_update_tick_step
    if(m_input_update_tick_step.has_ticked()) {
        // TODO: might be more like `calc_velocity_vector`.
        // 1. it is not exactly *input* but input mapped to normalized velocity vector
        // 2. I hate calling methods `update`
        glm::vec3 input = m_player_controller.input();

        if(m_player_entity.has_value()) {
            CharacterController* controller = m_world->registry().try_get<CharacterController>(m_player_entity.value());
            if(controller) {
                // controller->set_input(m_network_frame_idx, input);
                m_replicator_client.set_input(m_network_frame_idx, input);
                m_replicator_client.record_prediction(m_network_frame_idx);
            }
        }

        m_physics_world->step(m_network_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
        m_network_frame_idx++;
    }

    m_interpolator.interpolate_smoothed_entities(std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(500));

    if(m_player_entity.has_value()) {
        // after interpolation happen, follow the target
        // TODO: The third person controller could pull
        Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
        if(player_transform) {
            // m_player_controller.set_target(player_transform->position());
        }
    }

    m_frame_idx++;

    // m_replicator_client.update();
}

}
