/*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 "AIController.hpp"

#include "Engines/Physics/PhysicsWorld.hpp"
#include "Tools/Timer.hpp"
#include <algorithm>

namespace
{
    /// Returns whether an enemy is KO, useful to remove KO enemies
    bool isKO(const Player *enemy)
    {
        return (enemy == nullptr || enemy->isKo());
    }
}

AIController::AIController() : PlayerController(), PlayerCallbackReceiver(),
m_generator(std::random_device{}()), m_actionDist(0, 999),
m_targetEnemy(nullptr), m_behaviour(Behaviour::Offensive), m_attackPause(false)
{

}

void AIController::attach(Player &player)
{
    PlayerController::attach(player);
    player.addCallbackReceiver((*this));
}

void AIController::act()
{
    if (m_player == nullptr ||
        m_player->getAttack() != AttackIndex::Default ||
        m_player->isKo())
        return;

    const PhysicsWorld *physicsWorld = m_player->getPhysicsWorld();
    if (physicsWorld == nullptr)
        return;

    //Handling KO enemies (not to focus on a poor ko player ;))
    removeKoEnemies();

    if (m_targetEnemy == nullptr) //No target enemies so stop!
    {
        m_player->setMoving(MovingState::NotMoving);
        if (m_player->getState() == StateIndex::Crouched)
            m_player->uncrouch();
        return;
    }

    //Main AI Behaviour block
    //Handling target enemy, a 1% chance to randomly change target
    if (m_actionDist(m_generator) < 10)
    {
        updateTargetEnemy();
        if (m_targetEnemy == nullptr)
            return;
    }

    //Define the behaviour of the AI
    if (m_targetEnemy->getHealth() <= 25.f)
        m_behaviour = AIController::Behaviour::Offensive;
    else if (m_player->getHealth() <= 30.f)
        m_behaviour = AIController::Behaviour::Defensive;

    //Implement the different behaviours of the AI
    const Box &enemyBox = m_targetEnemy->getBox(), &egoBox = m_player->getBox();

    if (m_behaviour == AIController::Behaviour::Defensive)
    {
        //If the enemy is too close
        if (enemyBox.left - 300.f <= egoBox.left + egoBox.width &&
            enemyBox.left + enemyBox.width + 300.f >= egoBox.left)
        {
            const Box &worldBox = physicsWorld->getWorldBox();

            //Run away if possible
            const float worldRight = worldBox.left + worldBox.width;
            const float aiRight = egoBox.left + egoBox.width;
            const bool enemyLeft = (enemyBox.left <= egoBox.left);

            if (worldRight - aiRight >= 200.f &&
                egoBox.left - worldBox.left >= 200.f)
            {
                if (enemyBox.left + enemyBox.width < egoBox.left)
                    m_player->setMoving(MovingState::MovingRight);
                else if (egoBox.left + egoBox.width < enemyBox.left)
                    m_player->setMoving(MovingState::MovingLeft);

                if (m_actionDist(m_generator) < 35)
                    m_player->jump();
            }
            else //Or fight back
            {
                m_player->setMoving(MovingState::NotMoving);
                m_player->setLooksLeft(enemyLeft);

                const int action = m_actionDist(m_generator);
                if (action < 50) m_player->punch();
                else if (action < 100) m_player->kick();
                else if (action < 125) m_player->jump();
            }
        }
        else if (enemyBox.left - 450.f > egoBox.left + egoBox.width ||
                 enemyBox.left + enemyBox.width + 450.f < egoBox.left)
        {
            //If the distance between the AI and its enemy is fair enough
            randomDefensive();
        }
    }
    else if (m_behaviour == AIController::Behaviour::Offensive)
    {
        //Check if the enemy is above or the opposite
        const bool aboveEnemy = egoBox.top + egoBox.height <= enemyBox.top;
        const bool enemyAbove = egoBox.top >= enemyBox.top + enemyBox.height;

        if (aboveEnemy)
        {
            m_player->setMoving(m_player->getAheadDirection());
            if (m_actionDist(m_generator) < 10)
                m_player->jump();
        }
        else if (enemyAbove && m_actionDist(m_generator) < 50)
            m_player->setMoving(m_player->getBehindDirection());
        else
        {
            //X Flip to face the enemy
            if (enemyBox.left - 100.f > egoBox.left ||
                enemyBox.left + 100.f < egoBox.left)
            {
                m_player->setLooksLeft(enemyBox.left < egoBox.left);
            }

            //If the enemy is too far away, run to them
            if (enemyBox.left - 30.f > egoBox.left + egoBox.width ||
                enemyBox.left + enemyBox.width + 30.f < egoBox.left)
            {
                //Attack pauses handling! (sometimes make a break)
                if (Timer::getTime() - m_attackPause_start >=
                    std::chrono::milliseconds(300))
                {
                    m_attackPause = false;
                }

                if (!m_attackPause && m_actionDist(m_generator) < 40)
                {
                    m_player->setMoving(MovingState::NotMoving);

                    const int crouch_or_jump = m_actionDist(m_generator);
                    if (crouch_or_jump < 125 &&
                        m_player->getState() != StateIndex::Crouched)
                    {
                        m_player->crouch();
                    }
                    else if (crouch_or_jump < 190)
                        m_player->jump();

                    m_attackPause = true;
                    m_attackPause_start = Timer::getTime();
                }

                if (!m_attackPause) //Run to the enemy if no pause
                    m_player->setMoving(m_player->getAheadDirection());
            }
            else //Fight the enemy
            {
                const int action = m_actionDist(m_generator);
                if (action < 50) m_player->punch();
                else if (action < 100) m_player->kick();
                else if (action < 105) m_player->jump();
                else if (Timer::getTime() - m_attackState_start >=
                         std::chrono::milliseconds(300))
                {
                    m_player->setMoving(MovingState::NotMoving);
                    const int actionState = m_actionDist(m_generator);
                    if (actionState < 700 &&
                        m_player->getState() != StateIndex::Default)
                    {
                        m_player->uncrouch();
                    }
                    else if (actionState < 870 &&
                             m_player->getState() != StateIndex::Crouched)
                    {
                        m_player->crouch();
                    }

                    m_attackState_start = Timer::getTime();
                }
            }
        }
    }
}

void AIController::randomDefensive()
{
    if (m_player == nullptr || m_targetEnemy == nullptr)
        return;

    //Don't move for too long
    if (m_player->isMoving() && m_actionDist(m_generator) < 30)
    {
        m_player->setMoving(MovingState::NotMoving);
        m_player->setLooksLeft(m_targetEnemy->getBox().left <
                               m_player->getBox().left);
    }

    //Act randomly
    const int action = m_actionDist(m_generator);
    if (action < 40) m_player->setMoving(m_player->getAheadDirection());
    else if (action < 48) m_player->jump();
    else if (action < 56) m_player->punch();
    else if (action < 64) m_player->kick();
    else if (action < 72) m_player->throwShuriken();
}

void AIController::playerWasHit(float /*strength*/, HitDirection /*side*/,
                                const Player *source)
{
    //The AI was hit by a player, 80% probability to focus it
    if (source != nullptr && m_actionDist(m_generator) < 800)
        m_targetEnemy = source;
}

//Ignoring this callback here, only focus on impact from known other players
void AIController::playerHitTriggered() {}

void AIController::setEnemies(const std::vector<const Player*> &enemies)
{
    m_enemies.clear();
    for (const Player *enemy : enemies)
    {
        if (enemy != nullptr && enemy != m_player)
            m_enemies.push_back(enemy);
    }

    updateTargetEnemy();

    //Reset behaviour
    m_behaviour = Behaviour::Offensive;
}

void AIController::updateTargetEnemy()
{
    if (m_enemies.empty())
        m_targetEnemy = nullptr;
    else
    {
        std::uniform_int_distribution<std::size_t> enemyDist(0,
            m_enemies.size() - 1);
        m_targetEnemy = m_enemies[enemyDist(m_generator)];
    }
}

void AIController::removeKoEnemies()
{
    std::vector<const Player*>::const_iterator it_removed =
        std::remove_if(m_enemies.begin(), m_enemies.end(), isKO);

    if (it_removed != m_enemies.end())
    {
        m_enemies.erase(it_removed, m_enemies.end());
        updateTargetEnemy();
    }
}
