/*Copyright (C) 2010-2023 Duncan Deveaux

This file is part of Hikou no mizu.

Hikou no mizu is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Hikou no mizu is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Hikou no mizu.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "Player.hpp"
#include "Engines/Physics/PhysicsWorld.hpp"
#include "Engines/Sound/SoundInterface.hpp"
#include "Fight/DamageDealer.hpp"
#include "Network/Serialization/PlayerStatus.hpp"

#include "Tools/BuildValues.hpp" ///< Generated at build time
#include <pugixml.hpp>

namespace
{
    constexpr float MAX_HEALTH = 100.f;
}

Player::Player() : Player("", MAX_HEALTH, 0.f, 0.f, 0.f) {}

Player::Player(const std::string &name, float health, float strength,
               float speed, float jumpEnergy) :
PhysicsObject(), AnimationCallbackReceiver(), m_name(name), m_health(health),
m_strength(strength), m_baseSpeed(speed), m_speed(speed),
m_jumpEnergy(jumpEnergy), m_lookingLeft(false),
m_soundInterface(nullptr), m_damageTracker(nullptr),
m_requestsMove(MovingState::NotMoving), m_awaitsImpact(false)
{

}

void Player::initWeapons(WeaponEther &ether)
{
    m_weapons = WeaponInventory((*this), ether);
}

void Player::loadMoves()
{
    const std::string movesPath = "gfx/characters/" + m_name + "/";

    m_moves.load(movesPath + "data.xml", movesPath, (*this));
    m_moves.update(0); //Initial update (origin, ...)

    //Initialize the physics box width and height
    m_box.width = m_moves.getWidth();
    m_box.height = m_moves.getHeight();
}

void Player::reset(bool maintainPosition)
{
    setHealth(MAX_HEALTH);

    m_moves.reset();
    updatePhysicsBox();

    m_requestsMove = MovingState::NotMoving;
    m_awaitsImpact = false;
    m_lookingLeft = false;

    if (!maintainPosition)
    {
        m_box.left = 0.f; //Force reset the player position to ensure it is
        m_box.top = 0.f; //within the arena bounds in case of arena change
    }

    m_weapons.reset();
}

void Player::applyPlayerStatus(const PlayerStatus &status)
{
    //Health points
    setHealth(status.getHealth());

    //Direction and moving state
    m_requestsMove = status.getMovingState();
    m_lookingLeft = status.getLooksLeft();

    //Player moves
    m_moves.force(status.getState(),
                  status.getAttack(),
                  status.getAnimationTime());

    //Force update physics box
    m_box.width = status.getSize().x;
    m_box.height = status.getSize().y;
    applyPhysicsStatus(status);

    //Weapons
    m_weapons.applyStatus(status.getInventoryStatus());
}

void Player::update(float frameTime)
{
    m_moves.update(frameTime);
    updatePhysicsBox();

    if (!isKo() && !isHit())
    {
        std::string groundMaterial;
        bool onGround = isOnGround(&groundMaterial);

        //Impact sounds
        if (!onGround) m_awaitsImpact = true;
        else if (m_awaitsImpact)
        {
            m_awaitsImpact = false;
            if (groundMaterial != MATERIAL_NONE)
                playSoundEffect("impact_" + groundMaterial);
        }

        //Jumping
        if (onGround && m_moves.getState() == StateIndex::Jumping)
            updateState(StateIndex::Default);
        else if (!onGround && m_moves.getState() != StateIndex::Jumping)
            updateState(StateIndex::Jumping);

        //Moving
        if (m_requestsMove != MovingState::NotMoving)
        {
            setMoving(m_requestsMove); //Update moving state
            moveStep(frameTime); //Perform the movement
        }
    }
}

void Player::updateState(StateIndex state, bool force)
{
    m_moves.setState(state, force);
    updatePhysicsBox();
}

void Player::updateAttack(AttackIndex attack, bool force)
{
    //Notify that a hit was triggered from any source
    //Useful for camera shaking effects
    if (attack == AttackIndex::TakeAHit && force)
    {
        for (PlayerCallbackReceiver *receiver : m_receivers)
            receiver->playerHitTriggered();
    }

    m_moves.setAttack(attack, force);
    updatePhysicsBox();
}

void Player::updatePhysicsBox()
{
    setWidth(m_moves.getWidth(), looksLeft());
    setHeight(m_moves.getHeight(), true);
}

void Player::addCallbackReceiver(PlayerCallbackReceiver &receiver)
{
    m_receivers.push_back(&receiver);
}

void Player::setSoundInterface(SoundInterface *soundInterface)
{
    m_soundInterface = soundInterface;
}

void Player::setDamageTracker(DamageTracker &damageTracker)
{
    m_damageTracker = &damageTracker;
}

//Moves
void Player::moveStep(float frameTime)
{
    if (!isKo() && !isHit()) //Move
    {
        const float elapsedTime = frameTime / 1000.f;

        if (m_requestsMove == MovingState::MovingLeft)
            moveXPosition(-m_speed * elapsedTime);
        else if (m_requestsMove == MovingState::MovingRight)
            moveXPosition(m_speed * elapsedTime);
    }
}

void Player::setMoving(MovingState direction)
{
    if (!isKo() && !isHit() && m_moves.getAttack() == AttackIndex::Default)
    {
        m_requestsMove = direction;

        //Update looking direction
        if (m_requestsMove == MovingState::MovingLeft)
            m_lookingLeft = true;
        else if (m_requestsMove == MovingState::MovingRight)
            m_lookingLeft = false;

        //Update player state immediately
        if (m_requestsMove == MovingState::NotMoving &&
            m_moves.getState() == StateIndex::Moving)
        {
            updateState(StateIndex::Default);
        }
        else if (m_requestsMove != MovingState::NotMoving && isOnGround() &&
                 m_moves.getState() != StateIndex::Moving)
        {
            updateState(StateIndex::Moving);
        }
    }
}

void Player::jump()
{
    if (m_physicsWorld != nullptr && !isKo() && !isHit() &&
        m_moves.getState() != StateIndex::Crouched && isOnGround() &&
        m_velocity.y >= 0.f)
    {
        m_physicsWorld->pulseObject((*this), 0.f, -m_jumpEnergy);
    }
}

void Player::crouch()
{
    if (!isKo() && !isHit() && isOnGround())
        updateState(StateIndex::Crouched);
}

void Player::uncrouch()
{
    if (!isKo() && !isHit() && m_moves.getState() == StateIndex::Crouched)
        updateState(StateIndex::Default);
}

void Player::cancelAttack()
{
    updateAttack(AttackIndex::Default, true);
}

void Player::punch()
{
    if (!isKo() && m_moves.mayAttack())
        updateAttack(AttackIndex::Punch);
}

void Player::kick()
{
    if (!isKo() && m_moves.mayAttack())
        updateAttack(AttackIndex::Kick);
}

void Player::throwShuriken()
{
    if (m_physicsWorld != nullptr && !isKo() && m_moves.mayAttack())
        m_weapons.throwShuriken((*m_physicsWorld));
}

//Attacks callback
void Player::hit(const HitBox &hitBox)
{
    if (m_damageTracker != nullptr)
    {
        //Define the hit box
        Box relativeBox = hitBox.box;
        relativeBox.left = m_box.left + relativeBox.left;
        relativeBox.top = m_box.top + relativeBox.top;

        if (looksLeft())
        {
            relativeBox.left = m_box.left -
                ((relativeBox.left + relativeBox.width) -
                 (m_box.left + m_box.width));
        }

        //Hit objects
        const std::vector<PhysicsObject*> hitObjects = hitTest(relativeBox);
        for (PhysicsObject *target : hitObjects) //Hit!
        {
            HitDirection direction = (m_box.left < target->getBox().left) ?
                          HitDirection::FromLeft : HitDirection::FromRight;

            target->pushDamage(m_strength * hitBox.factor,
                               direction, (*this), (*m_damageTracker));
        }
    }
}

void Player::playSoundEffect(const std::string &effectName)
{
    if (m_soundInterface != nullptr)
        m_soundInterface->playSoundEffect(effectName, m_name);
}

//Animations callback
void Player::stepImpactCallback()
{
    std::string stepMaterial = MATERIAL_NONE;
    if (isOnGround(&stepMaterial))
    {
        if (stepMaterial != MATERIAL_NONE)
            playSoundEffect("walk_" + stepMaterial);
    }
}

//Physics callback
void Player::collideWorld(bool /*ground*/) {} //Nothing to do :)
void Player::collide(PhysicsObject& /*obj*/) {} //Nothing to do :)
void Player::takeAHit(float strength, HitDirection side,
                      const Player *source)
{
    //Alter health
    const float remainingHealth = m_health - strength;
    setHealth(remainingHealth);

    //Pulse
    if (m_physicsWorld != nullptr)
    {
        m_physicsWorld->pulseObject((*this),
            side == HitDirection::FromLeft ? 40.f * strength : -40.f * strength,
            0.f);
    }

    //Launch KO animation
    if (!isKo() && remainingHealth <= 0.f)
        updateState(StateIndex::Falling, true);
    else if (!isKo() && !isHit()) //Or launch hit animation
        updateAttack(AttackIndex::TakeAHit, true);

    for (PlayerCallbackReceiver *receiver : m_receivers)
        receiver->playerWasHit(strength, side, source);
}

void Player::pushDamage(float strength, HitDirection side,
                        const Player &source, DamageTracker &damage)
{
    damage.push(Damage(strength, side, source, (*this)));
}

//Get and set methods
const std::string &Player::getName() const
{
    return m_name;
}

void Player::setHealth(float health)
{
    if (health < 0.f) m_health = 0.f;
    else if (health > MAX_HEALTH) m_health = MAX_HEALTH;
    else m_health = health;
}

float Player::getHealth() const
{
    return m_health;
}

float Player::getBaseSpeed() const
{
    return m_baseSpeed;
}

void Player::setSpeed(float speed)
{
    if (speed > 0)
        m_speed = speed;
}

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

unsigned int Player::getWeaponsRemaining(WeaponType weaponType) const
{
    return m_weapons.getRemaining(weaponType);
}

AttackIndex Player::getAttack() const
{
    return m_moves.getAttack();
}

StateIndex Player::getState() const
{
    return m_moves.getState();
}

const Animation *Player::getAnimation() const
{
    return m_moves.getAnimation();
}

std::map<StateIndex, const State*> Player::getStateInfo() const
{
    return m_moves.getStateInfo();
}

bool Player::isKo() const
{
    return (m_moves.getState() == StateIndex::Falling ||
            m_moves.getState() == StateIndex::KO);
}

bool Player::isHit() const
{
    return (m_moves.getAttack() == AttackIndex::TakeAHit);
}

bool Player::isMoving() const
{
    return (m_requestsMove != MovingState::NotMoving);
}

MovingState Player::getMovingState() const
{
    return m_requestsMove;
}

MovingState Player::getAheadDirection() const
{
    return looksLeft() ? MovingState::MovingLeft : MovingState::MovingRight;
}

MovingState Player::getBehindDirection() const
{
    return looksLeft() ? MovingState::MovingRight : MovingState::MovingLeft;
}

void Player::setLooksLeft(bool looksLeft, bool force)
{
    if (force || !isMoving())
        m_lookingLeft = looksLeft;
}

bool Player::looksLeft() const
{
    return m_lookingLeft;
}

//Static methods
Player *Player::allocFromXML(const std::string &name,
                             const std::string &xmlRelPath)
{
    //pugixml initialization
    //Locate absolute path to xml data
    std::string xmlPath = BuildValues::data(xmlRelPath);

    pugi::xml_document xmlFile;
    if (!xmlFile.load_file(xmlPath.c_str()))
        return nullptr;

    pugi::xml_node infoNode;
    for (infoNode = xmlFile.child("main").child("character");
         infoNode;
         infoNode = infoNode.next_sibling("character"))
    {
        std::string itemName(infoNode.attribute("name").value());
        if (itemName == name)
        {
            //Get information
            float strength = infoNode.attribute("strength").as_float(10.f),
                  speed = infoNode.attribute("speed").as_float(700.f),
                  jumpEnergy = infoNode.attribute("jumpEnergy").as_float(750.f);

            //Create player
            return new Player(name, 100.f, strength, speed, jumpEnergy);
        }
    }

    return nullptr;
}
