//
//  SuperTuxKart - a fun racing game with go-kart
//  Copyright (C) 2011-2015 Joerg Henrichs
//
//  This program 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.
//
//  This program 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 this program; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

// done: be able to squash karts
// TODO: use a proportional corrector for avoiding brutal movements
// TODO: make the swatter (and other items) appear and disappear progressively
// done: remove the maximum number of squashes
// TODO: add a swatter music
// TODO: be able to squash items
// TODO: move some constants to KartProperties, use all constants from KartProperties

#include "items/swatter.hpp"

#include "achievements/achievements_status.hpp"
#include "audio/sfx_base.hpp"
#include "audio/sfx_manager.hpp"
#include "config/player_manager.hpp"
#include "graphics/explosion.hpp"
#include "graphics/irr_driver.hpp"
#include "io/file_manager.hpp"
#include "items/attachment_manager.hpp"
#include "items/projectile_manager.hpp"
#include "karts/abstract_kart.hpp"
#include "karts/controller/controller.hpp"
#include "karts/explosion_animation.hpp"
#include "karts/kart_properties.hpp"
#include "modes/capture_the_flag.hpp"
#include "network/network_string.hpp"
#include "network/rewind_manager.hpp"

#define SWAT_POS_OFFSET        core::vector3df(0.0, 0.2f, -0.4f)
#define SWAT_ANGLE_MIN  45
#define SWAT_ANGLE_MAX  135
#define SWAT_ANGLE_OFFSET (90.0f + 15.0f)
#define SWATTER_ANIMATION_SPEED 100.0f

// ----------------------------------------------------------------------------
/** Constructor: creates a swatter at a given attachment for a kart. If there
 *  was a bomb attached, it triggers the replace bomb animations.
 *  \param attachment The attachment instance where the swatter is attached to.
 *  \param kart The kart to which the swatter is attached.
 *  \param bomb_ticks Remaining bomb time in ticks, -1 if none.
 *  \param ticks Swatter duration.
 *  \param attachment class attachment from karts.
 */
Swatter::Swatter(AbstractKart *kart, int16_t bomb_ticks, int ticks,
                  Attachment* attachment)
       : AttachmentPlugin(kart, attachment)
{
    m_animation_phase  = SWATTER_AIMING;
    m_discard_now      = false;
    m_closest_kart     = NULL;
    m_discard_ticks    = World::getWorld()->getTicksSinceStart() + ticks;
    m_bomb_remaining   = bomb_ticks;
    m_scene_node       = NULL;
    m_bomb_scene_node  = NULL;
    m_swatter_duration = stk_config->time2Ticks(
        kart->getKartProperties()->getSwatterDuration());
    if (m_bomb_remaining != -1)
    {
        // There are 40 frames in blender for swatter_anim.blender
        // so 40 / 25 * 120
        m_discard_ticks =
            World::getWorld()->getTicksSinceStart() +
            stk_config->time2Ticks(40.0f / 25.0f);
    }
    m_swat_sound = NULL;
    m_swatter_animation_ticks = 0;
    m_played_swatter_animation = false;
}   // Swatter

// ----------------------------------------------------------------------------
/** Destructor, stops any playing sfx.
 */
Swatter::~Swatter()
{
    if(m_bomb_scene_node)
    {
        irr_driver->removeNode(m_bomb_scene_node);
        m_bomb_scene_node = NULL;
    }
#ifndef SERVER_ONLY
    if (m_swat_sound)
    {
        m_swat_sound->deleteSFX();
    }
#endif
}   // ~Swatter

// ----------------------------------------------------------------------------
void Swatter::updateGraphics(float dt)
{
#ifndef SERVER_ONLY
    if (m_bomb_remaining != -1)
    {
        if (!m_scene_node)
        {
            m_scene_node = m_kart->getAttachment()->getNode();
            m_scene_node->setPosition(SWAT_POS_OFFSET);
            m_scene_node->setMesh(attachment_manager
                ->getMesh(Attachment::ATTACH_SWATTER_ANIM));
            m_scene_node->setRotation(core::vector3df(0.0, -180.0, 0.0));
            m_scene_node->setAnimationSpeed(0.9f);
            m_scene_node->setCurrentFrame(0.0f);
            m_scene_node->setLoopMode(false);
        }
        if (!m_bomb_scene_node)
        {
            m_bomb_scene_node = irr_driver->addAnimatedMesh(
                attachment_manager->getMesh(Attachment::ATTACH_BOMB), "bomb");
#ifdef DEBUG
            std::string debug_name = m_kart->getIdent() + " (attachment)";
            m_bomb_scene_node->setName(debug_name.c_str());
#endif
            m_bomb_scene_node->setParent(m_kart->getNode());
            float time_left = stk_config->ticks2Time(m_bomb_remaining);
            if (time_left <= (m_bomb_scene_node->getEndFrame() -
                m_bomb_scene_node->getStartFrame() - 1))
            {
                m_bomb_scene_node->setCurrentFrame(
                    m_bomb_scene_node->getEndFrame()
                    - m_bomb_scene_node->getStartFrame() - 1 - time_left);
            }
            m_bomb_scene_node->setAnimationSpeed(0.0f);
        }

        float swat_bomb_frame = stk_config->ticks2Time(
            World::getWorld()->getTicksSinceStart() -
            (m_discard_ticks - stk_config->time2Ticks(40.0f / 25.0f)))
            * 25.0f;

        if (swat_bomb_frame >= (float)m_scene_node->getEndFrame())
            swat_bomb_frame = (float)m_scene_node->getEndFrame();

        m_scene_node->setRotation(core::vector3df(0.0, -180.0, 0.0));
        m_scene_node->setCurrentFrame(swat_bomb_frame);

        if (swat_bomb_frame >= 32.5f && m_bomb_scene_node != NULL)
        {
            m_bomb_scene_node->setPosition(
                core::vector3df(-(swat_bomb_frame - 32.5f), 0.0f, 0.0f));
            m_bomb_scene_node->setRotation(
                core::vector3df(-(swat_bomb_frame - 32.5f), 0.0f, 0.0f));
        }

        if (swat_bomb_frame >= 35)
        {
            m_bomb_scene_node->setVisible(false);
        }   // bom_frame > 35
    }   // if removing bomb
    else
    {
        if (!m_scene_node)
        {
            m_scene_node = m_kart->getAttachment()->getNode();
            if (m_kart->getIdent() == "nolok")
            {
                m_scene_node->setMesh(attachment_manager
                    ->getMesh(Attachment::ATTACH_NOLOKS_SWATTER));
            }
            else
            {
                m_scene_node->setMesh(attachment_manager
                    ->getMesh(Attachment::ATTACH_SWATTER));
            }
            m_scene_node->setPosition(SWAT_POS_OFFSET);
            m_scene_node->setLoopMode(false);
            m_scene_node->setAnimationSpeed(0.0f);
        }
        if (!m_swat_sound)
        {
            if (m_kart->getIdent() == "nolok")
                m_swat_sound = SFXManager::get()->createSoundSource("hammer");
            else
                m_swat_sound = SFXManager::get()->createSoundSource("swatter");
        }
        if (!m_discard_now)
        {
            switch (m_animation_phase)
            {
            case SWATTER_AIMING:
                {
                    pointToTarget();
                    m_played_swatter_animation = false;
                }
                break;
            case SWATTER_TO_TARGET:
                {
                    if (!m_played_swatter_animation)
                    {
                        m_played_swatter_animation = true;
                        // Setup the animation
                        m_scene_node->setCurrentFrame(0.0f);
                        m_scene_node->setAnimationSpeed(SWATTER_ANIMATION_SPEED);
                        Vec3 swatter_pos =
                            m_kart->getTrans()(Vec3(SWAT_POS_OFFSET));
                        m_swat_sound->setPosition(swatter_pos);
                        m_swat_sound->play();
                    }
                    pointToTarget();
                }
                break;
            case SWATTER_FROM_TARGET:
                break;
            }
        }
    }
#endif
}   // updateGraphics

// ----------------------------------------------------------------------------
/** Updates an armed swatter: it checks for any karts that are close enough
 *  and not invulnerable, it swats the kart.
 *  \param ticks Time step size.
 *  \return True if the attachment should be discarded.
 */
bool Swatter::updateAndTestFinished(int ticks)
{
    const int ticks_start = World::getWorld()->getTicksSinceStart();
    if (World::getWorld()->getTicksSinceStart() > m_discard_ticks)
        return true;

    // If removing bomb animation playing, it will only ends when
    // m_discard_ticks > world ticks
    if (m_bomb_remaining != -1)
        return false;

    if (!m_discard_now)
    {
        switch (m_animation_phase)
        {
        case SWATTER_AIMING:
            {
                // Avoid swatter near the start and the end lifetime of swatter
                // to make sure all clients know the existence of swatter each other
                if (m_swatter_duration - m_attachment->getTicksLeft() < 60 ||
                    m_attachment->getTicksLeft() < 90) // ~20 and ~60 below
                    return false;

                chooseTarget();
                if (!m_closest_kart)
                    break;

                // Get the node corresponding to the joint at the center of the
                // swatter (by swatter, I mean the thing hold in the hand, not
                // the whole thing)
                // The joint node doesn't update in server without graphics,
                // so an approximate position is used
                //scene::ISceneNode* swatter_node =
                //    m_scene_node->getJointNode("Swatter");
                //assert(swatter_node);
                //Vec3 swatter_pos = swatter_node->getAbsolutePosition();
                Vec3 swatter_pos = m_kart->getTrans()(Vec3(SWAT_POS_OFFSET));

                float dist2 = (m_closest_kart->getXYZ()-swatter_pos).length2();
                float min_dist2
                     = m_kart->getKartProperties()->getSwatterDistance();

                if (dist2 < min_dist2 && !m_kart->isGhostKart())
                {
                    // Start squashing
                    m_animation_phase = SWATTER_TO_TARGET;
                    m_swatter_animation_ticks =
                        m_attachment->getTicksLeft() - 20;
                }
            }
            break;
        case SWATTER_TO_TARGET:
            {
                // Did we just finish the first part of the movement?
                if (m_attachment->getTicksLeft() < m_swatter_animation_ticks &&
                    m_attachment->getTicksLeft() > 60)
                {
                    // Squash the karts and items around and
                    // change the current phase
                    squashThingsAround();
                    m_animation_phase = SWATTER_FROM_TARGET;
                    const int end_ticks = ticks_start + 60;
                    if (RaceManager::get()->isBattleMode() ||
                        RaceManager::get()->isSoccerMode())
                    {
                        // Remove swatter from kart in arena gameplay
                        // after one successful hit
                        m_discard_now = true;
                        m_discard_ticks = end_ticks;
                    }
                    m_swatter_animation_ticks =
                        m_attachment->getTicksLeft() - 60;
                }
            }
            break;
        case SWATTER_FROM_TARGET:
            {
                if (m_attachment->getTicksLeft() < m_swatter_animation_ticks &&
                    m_attachment->getTicksLeft() > 0)
                    m_animation_phase = SWATTER_AIMING;
            break;
            }
        }
    }
    return false;
}   // updateAndTestFinished

// ----------------------------------------------------------------------------
/** Determine the nearest kart or item and update the current target
 *  accordingly.
 */
void Swatter::chooseTarget()
{
    // TODO: for the moment, only handle karts...
    const World*  world         = World::getWorld();
    AbstractKart* closest_kart  = NULL;
    float         min_dist2     = FLT_MAX;

    for(unsigned int i=0; i<world->getNumKarts(); i++)
    {
        AbstractKart *kart = world->getKart(i);
        // TODO: isSwatterReady(), isSquashable()?
        if(kart->isEliminated() || kart==m_kart || kart->getKartAnimation())
            continue;
        // don't squash an already hurt kart
        if (kart->isInvulnerable() || kart->isSquashed())
            continue;

        // Don't hit teammates in team world
        if (world->hasTeam() &&
            world->getKartTeam(kart->getWorldKartId()) ==
            world->getKartTeam(m_kart->getWorldKartId()))
            continue;

        float dist2 = (kart->getXYZ()-m_kart->getXYZ()).length2();
        if(dist2<min_dist2)
        {
            min_dist2 = dist2;
            closest_kart = kart;
        }
    }
    // Not larger than 2^5 - 1 for kart id for optimizing state saving
    if (closest_kart && closest_kart->getWorldKartId() < 31)
        m_closest_kart = closest_kart;
    else
        m_closest_kart = NULL;
}

// ----------------------------------------------------------------------------
/** If there is a current target, point in its direction, otherwise adopt the
 *  default position. */
void Swatter::pointToTarget()
{
#ifndef SERVER_ONLY
    if (m_kart->isGhostKart() || !m_scene_node)
        return;

    if (!m_closest_kart)
    {
        m_scene_node->setRotation(core::vector3df());
    }
    else
    {
        Vec3 swatter_to_target =
            m_kart->getTrans().inverse()(m_closest_kart->getXYZ());
        float dy = -swatter_to_target.getZ();
        float dx = swatter_to_target.getX();
        float angle = SWAT_ANGLE_OFFSET + atan2f(dy, dx) * 180 / M_PI;
        m_scene_node->setRotation(core::vector3df(0.0, angle, 0.0));
    }
#endif
}   // pointToTarget

// ----------------------------------------------------------------------------
/** Squash karts or items that are around the end position (determined using
 *  a joint) of the swatter.
 */
void Swatter::squashThingsAround()
{
    if (m_kart->isGhostKart()) return;

    const KartProperties *kp = m_kart->getKartProperties();

    float duration = kp->getSwatterSquashDuration();
    float slowdown =  kp->getSwatterSquashSlowdown();
    // The squash attempt may fail because of invulnerability, shield, etc.
    // Making a bomb explode counts as a success
    bool success = m_closest_kart->setSquash(duration, slowdown);
    const bool has_created_explosion_animation =
        success && m_closest_kart->getKartAnimation() != NULL;

    if (success)
    {
        World::getWorld()->kartHit(m_closest_kart->getWorldKartId(),
            m_kart->getWorldKartId());

        CaptureTheFlag* ctf = dynamic_cast<CaptureTheFlag*>(World::getWorld());
        if (ctf)
        {
            int reset_ticks = (ctf->getTicksSinceStart() / 10) * 10 + 80;
            ctf->resetKartForSwatterHit(m_closest_kart->getWorldKartId(),
                reset_ticks);
        }
        // Handle achievement if the swatter is used by the current player
        if (m_kart->getController()->canGetAchievements())
        {
            PlayerManager::addKartHit(m_closest_kart->getWorldKartId());
            PlayerManager::increaseAchievement(AchievementsStatus::SWATTER_HIT, 1);
            PlayerManager::increaseAchievement(AchievementsStatus::ALL_HITS, 1);
            if (RaceManager::get()->isLinearRaceMode())
            {
                PlayerManager::increaseAchievement(AchievementsStatus::SWATTER_HIT_1RACE, 1);
                PlayerManager::increaseAchievement(AchievementsStatus::ALL_HITS_1RACE, 1);
            }
        }
    }

    if (!GUIEngine::isNoGraphics() && has_created_explosion_animation &&
        !RewindManager::get()->isRewinding())
    {
        HitEffect *he = new Explosion(m_kart->getXYZ(),  "explosion", "explosion.xml");
        if(m_kart->getController()->isLocalPlayerController())
            he->setLocalPlayerKartHit();
        ProjectileManager::get()->addHitEffect(he);
    }   // if kart has bomb attached

    // TODO: squash items
}   // squashThingsAround

// ----------------------------------------------------------------------------
void Swatter::restoreState(BareNetworkString* buffer)
{
    int16_t prev_bomb_remaing = m_bomb_remaining;
    m_bomb_remaining = buffer->getUInt16();
    if (prev_bomb_remaing != m_bomb_remaining)
    {
        // Wrong state, clear mesh and let updateGraphics reset itself
        m_scene_node = NULL;
        if (m_bomb_scene_node)
        {
            irr_driver->removeNode(m_bomb_scene_node);
            m_bomb_scene_node = NULL;
        }
    }
    if (m_bomb_remaining == -1)
    {
        uint8_t combined = buffer->getUInt8();
        int kart_id = combined & 31;
        if (kart_id == 31)
            m_closest_kart = NULL;
        else
            m_closest_kart = World::getWorld()->getKart(kart_id);
        m_animation_phase = AnimationPhase((combined >> 5) & 3);
        m_discard_now = (combined >> 7) == 1;
        m_discard_ticks = buffer->getUInt32();
        m_swatter_animation_ticks = buffer->getUInt16();
    }
    else
        m_discard_ticks = buffer->getUInt32();
}   // restoreState

// ----------------------------------------------------------------------------
void Swatter::saveState(BareNetworkString* buffer) const
{
    buffer->addUInt16(m_bomb_remaining);
    if (m_bomb_remaining == -1)
    {
        uint8_t combined =
            m_closest_kart ? (uint8_t)m_closest_kart->getWorldKartId() : 31;
        combined |= m_animation_phase << 5;
        combined |= (m_discard_now ? (1 << 7) : 0);
        buffer->addUInt8(combined).addUInt32(m_discard_ticks)
            .addUInt16(m_swatter_animation_ticks);
    }
    else
        buffer->addUInt32(m_discard_ticks);
}   // saveState
