/*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 "PlayerMoves.hpp"
#include "Player.hpp"

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

namespace
{
    /// 100ms between each attack
    constexpr std::chrono::milliseconds RECOVERY_TIME(100);
    constexpr float ANIMATION_SCALE = .8f;
}

PlayerMoves::PlayerMoves() : m_state(StateIndex::Default),
m_attack(AttackIndex::Default), m_targetState(StateIndex::None),
m_lastAttackEndTime(-RECOVERY_TIME)
{

}

void PlayerMoves::reset()
{
    setState(StateIndex::Default, true);
    m_lastAttackEndTime = Timer::TimePoint(-RECOVERY_TIME);
}

void PlayerMoves::update(float frameTime)
{
    if (m_attack != AttackIndex::Default) //Attack launched
    {
        m_statesList[m_state].animation.reset();
        m_statesList[m_state].attacks[m_attack].update(frameTime);

        if (m_statesList[m_state].attacks[m_attack].ready()) //finished
        {
            m_attack = AttackIndex::Default;
            m_lastAttackEndTime = Timer::getTime();
            return;
        }
    }
    else
    {
        CallbackAnimation &animation = m_statesList[m_state].animation;
        if (animation.goInLoop(frameTime / 1000.f) &&
            m_targetState != StateIndex::None)
        {
            animation.reset();
            setState(m_targetState);
            m_targetState = StateIndex::None;
        }
    }
}

bool PlayerMoves::mayAttack()
{
    return m_attack == AttackIndex::Default &&
          (Timer::getTime() - m_lastAttackEndTime) >= RECOVERY_TIME;
}

void PlayerMoves::setAttack(AttackIndex attack, bool force)
{
    if ((force || mayAttack()) && (attack == AttackIndex::Default ||
        m_statesList[m_state].attacks.count(attack) == 1))
    {
        if (m_attack != AttackIndex::Default)
            m_statesList[m_state].attacks[m_attack].reset();

        m_attack = attack;
    }
}

void PlayerMoves::setState(StateIndex state, bool force)
{
    //Forcing to change state
    //(basically in case of KO : current attacks must not interfere)
    if (force && m_attack != AttackIndex::Default)
    {
        //Reset current attack and properly go back to default action
        m_statesList[m_state].attacks[m_attack].reset();
        m_attack = AttackIndex::Default;
    }

    if (m_attack == AttackIndex::Default)
    {
        m_state = state;
        m_statesList[m_state].animation.reset();
        m_targetState = (m_state == StateIndex::Falling) ? StateIndex::KO
                                                         : StateIndex::None;
    }
}

void PlayerMoves::setAnimationTime(float time)
{
    if (m_attack == AttackIndex::Default)
        m_statesList[m_state].animation.forceTime(time);
    else
        m_statesList[m_state].attacks[m_attack].forceTime(time);
}

void PlayerMoves::force(StateIndex state, AttackIndex attack, float animTime)
{
    if (m_state != state)
    {
        m_statesList[m_state].animation.reset(); //Reset current state
        m_state = state;
    }

    if (m_attack != attack)
    {
        m_statesList[m_state].attacks[m_attack].reset(); //Reset current attack
        m_attack = attack;
    }

    setAnimationTime(animTime);
}

AttackIndex PlayerMoves::getAttack() const
{
    return m_attack;
}

StateIndex PlayerMoves::getState() const
{
    return m_state;
}

const Animation *PlayerMoves::getAnimation() const
{
    try
    {
        if (m_attack == AttackIndex::Default)
            return &(m_statesList.at(m_state).animation);
        else
            return &(m_statesList.at(m_state).attacks.at(m_attack).getAnimation());
    }
    catch (const std::exception &) {}

    return nullptr;
}

std::map<StateIndex, const State*> PlayerMoves::getStateInfo() const
{
    std::map<StateIndex, const State*> stateInfo;

    std::map<StateIndex, State>::const_iterator it;
    for (it = m_statesList.begin(); it != m_statesList.end(); ++it)
        stateInfo[it->first] = &it->second;

    return stateInfo;
}

float PlayerMoves::getWidth() const
{
    const Animation *anim = getAnimation();
    if (anim == nullptr)
        return 0.f;

    Box bodyBox;
    anim->getBodyBox(bodyBox);

    return bodyBox.width * anim->getXScale();
}

float PlayerMoves::getHeight() const
{
    const Animation *anim = getAnimation();
    if (anim == nullptr)
        return 0.f;

    Box bodyBox;
    anim->getBodyBox(bodyBox);

    return bodyBox.height * anim->getYScale();
}

void PlayerMoves::load(const std::string &xmlRelPath,
                       const std::string &animationsPath, Player &player)
{
    //pugixml initialization
    //Locate absolute path to xml data
    const std::string xmlPath = BuildValues::data(xmlRelPath);

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

    pugi::xml_node stateNode;
    for (stateNode = xmlFile.child("main").child("data").first_child();
         stateNode;
         stateNode = stateNode.next_sibling())
    {
        const std::string stateName(stateNode.name());
        StateIndex stateKey;
        if (stateName == "default") stateKey = StateIndex::Default;
        else if (stateName == "jumpTransition") stateKey = StateIndex::JumpTransition;
        else if (stateName == "jumping") stateKey = StateIndex::Jumping;
        else if (stateName == "moving") stateKey = StateIndex::Moving;
        else if (stateName == "crouched") stateKey = StateIndex::Crouched;
        else if (stateName == "falling") stateKey = StateIndex::Falling;
        else if (stateName == "ko") stateKey = StateIndex::KO;
        else continue;

        State state;

        //Main animation
        pugi::xml_node animNode = stateNode.child("animation");

        const std::string animationPath = animationsPath +
            std::string(animNode.attribute("path").value()) + ".png";
        loadAnimation(animationPath, animNode, state.animation);
        state.animation.setReceiver(&player);

        //Attacks
        pugi::xml_node attackNode;
        for (attackNode = stateNode.child("attacks").first_child();
             attackNode;
             attackNode = attackNode.next_sibling())
        {
            const std::string attackName(attackNode.name());
            AttackIndex attackKey;
            if (attackName == "punch") attackKey = AttackIndex::Punch;
            else if (attackName == "kick") attackKey = AttackIndex::Kick;
            else if (attackName == "takeAHit") attackKey = AttackIndex::TakeAHit;
            else continue;

            Attack &attack = state.attacks[attackKey];
            attack.setPlayer(player);

            //Animation
            pugi::xml_node attackAnimNode = attackNode.child("animation");

            const std::string attackAnimPath = animationsPath +
                std::string(attackAnimNode.attribute("path").value()) + ".png";

            CallbackAnimation animation;
            loadAnimation(attackAnimPath, attackAnimNode, animation);
            animation.setReceiver(&player);

            attack.setAnimation(animation);

            //Hits
            pugi::xml_node hitNode;
            for (hitNode = attackNode.child("hits").child("hit");
                 hitNode;
                 hitNode = hitNode.next_sibling("hit"))
            {
                //Box
                Box hitBox;
                const float x = hitNode.attribute("x").as_float(0.f),
                            y = hitNode.attribute("y").as_float(0.f),
                            width = hitNode.attribute("width").as_float(-1.f),
                            height = hitNode.attribute("height").as_float(-1.f);

                if (width < 0.f || height < 0.f)
                    hitBox = Box(x - 5.f, y - 5.f, 10.f, 10.f);
                else
                    hitBox = Box(x, y, width, height);

                //Time & Strength factor
                const float hitTime = hitNode.attribute("time").as_float(0.f),
                    hitFactor = hitNode.attribute("strength").as_float(0.f);

                attack.addHitBox(HitBox(hitBox, hitTime, hitFactor));
            }

            //Scale the hit boxes according to the animation scale
            attack.scaleHitBoxes(ANIMATION_SCALE);

            //Sound effects
            pugi::xml_node soundeffectNode;
            for (soundeffectNode = attackNode.child("sfx").child("soundeffect");
                 soundeffectNode;
                 soundeffectNode = soundeffectNode.next_sibling("soundeffect"))
            {
                const float effectTime =
                    soundeffectNode.attribute("time").as_float(0.f);
                std::string effectName(
                    soundeffectNode.attribute("name").as_string(""));

                attack.addSoundEffect(effectTime, effectName);
            }
        }

        //Add state
        m_statesList[stateKey] = state;
    }
}

void PlayerMoves::loadAnimation(const std::string &path,
                                const pugi::xml_node &animNode,
                                CallbackAnimation &dst) const
{
    //Main parameters
    const float duration = animNode.attribute("duration").as_float(0.f);

    dst.setPath(path);
    dst.setDuration(duration);
    dst.setScale(ANIMATION_SCALE);

    //Frames
    pugi::xml_node frameNode;
    for (frameNode = animNode.child("frame");
         frameNode;
         frameNode = frameNode.next_sibling("frame"))
    {
        const unsigned int repeat = frameNode.attribute("repeat").as_uint(1);

        const int srcX = frameNode.attribute("x").as_int(0),
                  srcY = frameNode.attribute("y").as_int(0),
                  srcWidth = frameNode.attribute("width").as_int(0),
                  srcHeight = frameNode.attribute("height").as_int(0);

        const float bodyX = frameNode.attribute("bodyX").as_float(0.f),
            bodyY = frameNode.attribute("bodyY").as_float(0.f),
            bodyWidth = frameNode.attribute("bodyWidth").as_float(0.f),
            bodyHeight = frameNode.attribute("bodyHeight").as_float(0.f);

        const bool stepImpact =
            frameNode.attribute("stepImpact").as_bool(false);

        const Frame frame(Box(static_cast<float>(srcX),
                              static_cast<float>(srcY),
                              static_cast<float>(srcWidth),
                              static_cast<float>(srcHeight)),
                          Box(bodyX, bodyY, bodyWidth, bodyHeight));

        for (unsigned int i = 0; i < repeat; i++)
            dst.addFrame(frame);

        if (stepImpact)
            dst.addStepImpactCallback(dst.getFrameSize() - 1);
    }
}
