/*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 "WeaponEther.hpp"
#include "Weapon.hpp"
#include "Player/Player.hpp"
#include "Engines/Physics/PhysicsWorld.hpp"
#include "Engines/Sound/SoundInterface.hpp"
#include "Network/Serialization/EtherStatus.hpp"
#include "Structs/Vector.hpp"

#include "Weapon/Shuriken.hpp"

namespace
{
    constexpr float WEAPON_THROWING_DISTANCE_X = 50.f;
    constexpr float WEAPON_THROWING_DISTANCE_Y = 20.f;
}

WeaponEther::WeaponEther() :
m_weaponIx(0), m_callbackReceiver(nullptr), m_soundInterface(nullptr)
{

}

WeaponEther::~WeaponEther()
{
    std::map<unsigned int, Weapon*>::iterator it;
    for (it = m_weaponsList.begin(); it != m_weaponsList.end(); ++it)
        delete it->second;

    m_weaponsList.clear();
}

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

void WeaponEther::setCallbackReceiver(WeaponEtherCallbackReceiver *callbackReceiver)
{
    m_callbackReceiver = callbackReceiver;
}

void WeaponEther::processQueue()
{
    //Remove weapons to be removed
    for (unsigned int weaponID : m_toBeRemoved)
    {
        Weapon *weapon = m_weaponsList[weaponID];

        if (m_callbackReceiver != nullptr)
            m_callbackReceiver->weaponRemoved((*weapon));

        weapon->removeFromWorld();
        m_weaponsList.erase(weaponID);
        delete weapon;
    }

    m_toBeRemoved.clear();
}

void WeaponEther::update(float frameTime)
{
    processQueue();

    //Update graphics
    std::map<unsigned int, Weapon*>::iterator it;
    for (it = m_weaponsList.begin(); it !=  m_weaponsList.end(); ++it)
        it->second->update(frameTime);
}

void WeaponEther::queueForRemoval(unsigned int weaponID)
{
    if (m_weaponsList.count(weaponID) > 0 && m_toBeRemoved.count(weaponID) == 0)
        m_toBeRemoved.insert(weaponID);
}

void WeaponEther::clear()
{
    std::map<unsigned int, Weapon*>::iterator it;
    for (it = m_weaponsList.begin(); it != m_weaponsList.end(); ++it)
        queueForRemoval(it->first);

    processQueue();
    m_weaponIx = 0;
}

bool WeaponEther::throwWeapon(PhysicsWorld &world,
                              WeaponType weaponType, unsigned int weaponIx,
                              const Vector &position, const Vector &velocity)
{
    if (weaponType == WeaponType::Shuriken)
    {
        Shuriken *shuriken = new Shuriken((*this), weaponIx);
        shuriken->setPhysicsWorld(world);
        shuriken->setPosition(position.x, position.y);
        shuriken->setSoundInterface(m_soundInterface);

        m_weaponsList[weaponIx] = shuriken;
        world.addObject((*shuriken));
        world.pulseObject((*shuriken), velocity.x, velocity.y);

        if (m_callbackReceiver != nullptr)
            m_callbackReceiver->weaponAdded((*shuriken));

        return true;
    }

    return false;
}

bool WeaponEther::throwShuriken(PhysicsWorld &world, const Player &player)
{
    const Box &playerBox = player.getBox();
    const bool throwRight = !player.looksLeft();

    const float width = SHURIKEN_SIZE, height = SHURIKEN_SIZE;

    const float x_position = throwRight ?
        playerBox.left + playerBox.width + 10.f :
        playerBox.left - width - 10.f;
    const float y_position = playerBox.top + playerBox.height / 3.f;

    Box requiredBox(throwRight ? x_position :
                                 x_position - WEAPON_THROWING_DISTANCE_X,
                    y_position - WEAPON_THROWING_DISTANCE_Y,
                    width + WEAPON_THROWING_DISTANCE_X,
                    height + 2.f * WEAPON_THROWING_DISTANCE_Y );

    if (world.isFree(requiredBox))
    {
        Vector pulse(2700.f, -400.f);
        if (player.isMoving()) pulse.x += player.getSpeed() * .5f;
        if (!throwRight) pulse.x = -pulse.x;
        pulse = pulse + player.getVelocity() * .5f;

        return throwWeapon(world, WeaponType::Shuriken, m_weaponIx++,
                        Vector(x_position, y_position), pulse);
    }

    return false;
}

void WeaponEther::applyStatus(PhysicsWorld &world, const EtherStatus &status)
{
    std::vector<WeaponStatus> toAdd;

    std::map<unsigned int, Weapon*>::const_iterator it_known = m_weaponsList.begin();
    std::vector<WeaponStatus>::const_iterator it_updated = status.getActiveWeapons().begin();

    while (it_known != m_weaponsList.end() ||
           it_updated != status.getActiveWeapons().end())
    {
        if (it_known == m_weaponsList.end())
        {
            //Reached the end of the known weapons list:
            //Add the unknown weapon
            toAdd.push_back((*it_updated));
            ++it_updated;
        }
        else if (it_updated == status.getActiveWeapons().end())
        {
            //Reached the end of the updated weapons list:
            //Remove the remaining known weapons
            queueForRemoval(it_known->first);
            ++it_known;
        }
        else
        {
            const unsigned int knownId = it_known->first;
            const unsigned int updatedId = it_updated->getIdentifier();

            if (knownId == updatedId)
            {
                //Weapon identifier match: update the known weapon
                it_known->second->applyStatus((*it_updated));
                ++it_known;
                ++it_updated;
            }
            else if (knownId < updatedId)
            {
                //The known weapon identifier is not in the updated
                //weapons list (which is sorted):
                //It has been removed, remove it from the known weapons
                queueForRemoval(knownId);
                ++it_known;
            }
            else
            {
                //The updated weapon identifier is not in the
                //known weapons list (which is sorted):
                //It has been removed locally (due to the local
                //fight scene interpolation), add it again
                toAdd.push_back((*it_updated));
                ++it_updated;
            }
        }
    }

    //Add previously unknown weapons
    for (const WeaponStatus &addedWeapon : toAdd)
    {
        throwWeapon(world, addedWeapon.getType(), addedWeapon.getIdentifier(),
                    addedWeapon.getPosition(), addedWeapon.getVelocity());
    }
}

EtherStatus WeaponEther::getStatus() const
{
    EtherStatus status;
    status.readFromEther(m_weaponsList);

    return status;
}
