#include "Player.hpp"

#include <algorithm>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <utility>

#include "content/ContentReport.hpp"
#include "items/Inventory.hpp"
#include "Entities.hpp"
#include "rigging.hpp"
#include "physics/Hitbox.hpp"
#include "physics/PhysicsSolver.hpp"
#include "voxels/Chunks.hpp"
#include "window/Camera.hpp"
#include "window/Events.hpp"
#include "world/Level.hpp"
#include "data/dv_util.hpp"
#include "debug/Logger.hpp"

static debug::Logger logger("player");

constexpr float CROUCH_SPEED_MUL = 0.35f;
constexpr float RUN_SPEED_MUL = 1.5f;
constexpr float PLAYER_GROUND_DAMPING = 10.0f;
constexpr float PLAYER_AIR_DAMPING = 8.0f;
constexpr float FLIGHT_SPEED_MUL = 4.0f;
constexpr float CHEAT_SPEED_MUL = 5.0f;
constexpr float JUMP_FORCE = 8.0f;
constexpr int SPAWN_ATTEMPTS_PER_UPDATE = 64;

Player::Player(
    Level& level,
    int64_t id,
    const std::string& name,
    glm::vec3 position,
    float speed,
    std::shared_ptr<Inventory> inv,
    entityid_t eid
)
    : level(level),
      id(id),
      name(name),
      speed(speed),
      chosenSlot(0),
      position(position),
      inventory(std::move(inv)),
      eid(eid),
      chunks(std::make_unique<Chunks>(
          3, 3, 0, 0, level.events.get(), *level.content.getIndices()
      )),
      fpCamera(level.getCamera("core:first-person")),
      spCamera(level.getCamera("core:third-person-front")),
      tpCamera(level.getCamera("core:third-person-back")),
      currentCamera(fpCamera) {
    fpCamera->setFov(glm::radians(90.0f));
    spCamera->setFov(glm::radians(90.0f));
    tpCamera->setFov(glm::radians(90.0f));
}

Player::~Player() = default;

void Player::updateEntity() {
    if (eid == 0) {
        auto& def = level.content.entities.require("base:player");
        eid = level.entities->spawn(def, getPosition());
    } else if (auto entity = level.entities->get(eid)) {
        position = entity->getTransform().pos;
    } else if (chunks->getChunkByVoxel(position)) {
        logger.error() << "player entity despawned or deleted; "
                          "will be respawned";
        eid = 0;
    }
}

Hitbox* Player::getHitbox() {
    if (auto entity = level.entities->get(eid)) {
        return &entity->getRigidbody().hitbox;
    }
    return nullptr;
}

void Player::updateInput(PlayerInput& input, float delta) {
    auto hitbox = getHitbox();
    if (hitbox == nullptr) {
        return;
    }
    bool crouch = input.shift && hitbox->grounded && !input.sprint;
    float speed = this->speed;
    if (flight) {
        speed *= FLIGHT_SPEED_MUL;
    }
    if (input.cheat) {
        speed *= CHEAT_SPEED_MUL;
    }

    hitbox->crouching = crouch;
    if (crouch) {
        speed *= CROUCH_SPEED_MUL;
    } else if (input.sprint) {
        speed *= RUN_SPEED_MUL;
    }

    glm::vec3 dir(0, 0, 0);
    if (input.moveForward) {
        dir += fpCamera->dir;
    }
    if (input.moveBack) {
        dir -= fpCamera->dir;
    }
    if (input.moveRight) {
        dir += fpCamera->right;
    }
    if (input.moveLeft) {
        dir -= fpCamera->right;
    }
    if (glm::length(dir) > 0.0f) {
        dir = glm::normalize(dir);
        hitbox->velocity += dir * speed * delta * 9.0f;
    }

    hitbox->linearDamping = PLAYER_GROUND_DAMPING;
    hitbox->verticalDamping = flight;
    hitbox->gravityScale = flight ? 0.0f : 1.0f;
    if (flight) {
        hitbox->linearDamping = PLAYER_AIR_DAMPING;
        if (input.jump) {
            hitbox->velocity.y += speed * delta * 9;
        }
        if (input.shift) {
            hitbox->velocity.y -= speed * delta * 9;
        }
    }
    if (!hitbox->grounded) {
        hitbox->linearDamping = PLAYER_AIR_DAMPING;
    }

    if (input.jump && hitbox->grounded) {
        hitbox->velocity.y = JUMP_FORCE;
    }

    hitbox->type = noclip ? BodyType::KINEMATIC : BodyType::DYNAMIC;
}

void Player::updateSelectedEntity() {
    selectedEid = selection.entity;
}

void Player::postUpdate() {
    auto entity = level.entities->get(eid);
    if (!entity.has_value()) {
        return;
    }
    auto& hitbox = entity->getRigidbody().hitbox;
    position = hitbox.position;

    if (flight && hitbox.grounded && !noclip) {
        flight = false;
    }
    if (spawnpoint.y <= 0.1) {
        for (int i = 0; i < SPAWN_ATTEMPTS_PER_UPDATE; i++) {
            attemptToFindSpawnpoint();
        }
    }

    auto& skeleton = entity->getSkeleton();

    skeleton.visible = currentCamera != fpCamera;

    auto body = skeleton.config->find("body");
    auto head = skeleton.config->find("head");

    if (body) {
        skeleton.pose.matrices[body->getIndex()] = glm::rotate(
            glm::mat4(1.0f), glm::radians(rotation.x), glm::vec3(0, 1, 0)
        );
    }
    if (head) {
        skeleton.pose.matrices[head->getIndex()] = glm::rotate(
            glm::mat4(1.0f), glm::radians(rotation.y), glm::vec3(1, 0, 0)
        );
    }
}

void Player::teleport(glm::vec3 position) {
    this->position = position;

    if (auto entity = level.entities->get(eid)) {
        entity->getRigidbody().hitbox.position = position;
        entity->getTransform().setPos(position);
    }
}

void Player::attemptToFindSpawnpoint() {
    glm::vec3 newpos(
        position.x + (rand() % 200 - 100),
        rand() % 80 + 100,
        position.z + (rand() % 200 - 100)
    );
    while (newpos.y > 0 &&
           !chunks->isObstacleBlock(newpos.x, newpos.y - 2, newpos.z)) {
        newpos.y--;
    }

    voxel* headvox = chunks->get(newpos.x, newpos.y + 1, newpos.z);
    if (chunks->isObstacleBlock(newpos.x, newpos.y, newpos.z) ||
        headvox == nullptr || headvox->id != 0) {
        return;
    }
    spawnpoint = newpos + glm::vec3(0.5f, 0.0f, 0.5f);
    teleport(spawnpoint);
}

void Player::setChosenSlot(int index) {
    chosenSlot = index;
}

int Player::getChosenSlot() const {
    return chosenSlot;
}

float Player::getSpeed() const {
    return speed;
}

bool Player::isFlight() const {
    return flight;
}

void Player::setFlight(bool flag) {
    this->flight = flag;
}

bool Player::isNoclip() const {
    return noclip;
}

void Player::setNoclip(bool flag) {
    this->noclip = flag;
}

bool Player::isInfiniteItems() const {
    return infiniteItems;
}

void Player::setInfiniteItems(bool flag) {
    infiniteItems = flag;
}

bool Player::isInstantDestruction() const {
    return instantDestruction;
}

void Player::setInstantDestruction(bool flag) {
    instantDestruction = flag;
}

bool Player::isLoadingChunks() const {
    return loadingChunks;
}

void Player::setLoadingChunks(bool flag) {
    loadingChunks = flag;
}

entityid_t Player::getEntity() const {
    return eid;
}

void Player::setEntity(entityid_t eid) {
    this->eid = eid;
}

entityid_t Player::getSelectedEntity() const {
    return selectedEid;
}

void Player::setName(const std::string& name) {
    this->name = name;
}

const std::string& Player::getName() const {
    return name;
}

const std::shared_ptr<Inventory>& Player::getInventory() const {
    return inventory;
}

void Player::setSpawnPoint(glm::vec3 spawnpoint) {
    this->spawnpoint = spawnpoint;
}

glm::vec3 Player::getSpawnPoint() const {
    return spawnpoint;
}

dv::value Player::serialize() const {
    auto root = dv::object();

    root["id"] = id;
    root["name"] = name;

    root["position"] = dv::to_value(position);
    root["rotation"] = dv::to_value(rotation);
    root["spawnpoint"] = dv::to_value(spawnpoint);

    root["flight"] = flight;
    root["noclip"] = noclip;
    root["infinite-items"] = infiniteItems;
    root["instant-destruction"] = instantDestruction;
    root["loading-chunks"] = loadingChunks;
    root["chosen-slot"] = chosenSlot;
    root["entity"] = eid;
    root["inventory"] = inventory->serialize();
    auto found =
        std::find(level.cameras.begin(), level.cameras.end(), currentCamera);
    if (found != level.cameras.end()) {
        root["camera"] = level.content.getIndices(ResourceType::CAMERA)
                .getName(found - level.cameras.begin());
    }
    return root;
}

void Player::deserialize(const dv::value& src) {
    src.at("id").get(id);
    src.at("name").get(name);

    const auto& posarr = src["position"];

    dv::get_vec(posarr, position);
    fpCamera->position = position;

    const auto& rotarr = src["rotation"];
    dv::get_vec(rotarr, rotation);

    const auto& sparr = src["spawnpoint"];
    setSpawnPoint(glm::vec3(
        sparr[0].asNumber(), sparr[1].asNumber(), sparr[2].asNumber()));

    flight = src["flight"].asBoolean();
    noclip = src["noclip"].asBoolean();
    src.at("infinite-items").get(infiniteItems);
    src.at("instant-destruction").get(instantDestruction);
    src.at("loading-chunks").get(loadingChunks);

    setChosenSlot(src["chosen-slot"].asInteger());
    eid = src["entity"].asNumber();

    if (src.has("inventory")) {
        getInventory()->deserialize(src["inventory"]);
    }

    if (src.has("camera")) {
        std::string name = src["camera"].asString();
        if (auto camera = level.getCamera(name)) {
            currentCamera = camera;
        }
    }
}

void Player::convert(dv::value& data, const ContentReport* report) {
    if (data.has("players")) {
        auto& players = data["players"];
        for (uint i = 0; i < players.size(); i++) {
            auto& playerData = players[i];
            if (playerData.has("inventory")) {
                Inventory::convert(playerData["inventory"], report);
            }
        }

    } else if (data.has("inventory")){
        Inventory::convert(data["inventory"], report);
    }
}
