/* bzflag
 * Copyright (c) 1993-2025 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

#include "Plan.h"
#include "BZDBCache.h"
#include "playing.h"
#include "LocalPlayer.h"
#include "Roster.h"
#include "TargetingUtils.h"
#include "World.h"

#include <algorithm>

/**
 * Plan
 */
Plan::Plan(float planDuration)
{
    planExpiration = TimeKeeper::getCurrent();
    planExpiration += planDuration;
}


Plan::~Plan()
{
}


bool Plan::isValid()
{
    TimeKeeper now = TimeKeeper();
    float delta = float(now - planExpiration);
    return (delta < 0.0f);
}

void Plan::execute(float &, float &)
{
    float pos[3];
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    memcpy(pos, myTank->getPosition(), sizeof(pos));
    if (pos[2] < 0.0f)
        pos[2] = 0.01f;
    float myAzimuth = myTank->getAngle();

    float dir[3] = {cosf(myAzimuth), sinf(myAzimuth), 0.0f};
    pos[2] += myTank->getMuzzleHeight();
    Ray tankRay(pos, dir);
    pos[2] -= myTank->getMuzzleHeight();

    if (myTank->getFlag() == Flags::ShockWave)
    {
        TimeKeeper now = TimeKeeper::getTick();
        if (now - lastShot >= (1.0f / World::getWorld()->getMaxShots()))
        {
            bool hasSWTarget = false;
            for (int t = 0; t < curMaxPlayers; t++)
            {
                if (t != myTank->getId() && remotePlayers[t] &&
                        remotePlayers[t]->isAlive() && !remotePlayers[t]->isPaused() &&
                        !remotePlayers[t]->isNotResponding())
                {

                    const float *tp = remotePlayers[t]->getPosition();
                    float enemyPos[3];

                    //toss in some lag adjustment/future prediction - 300 millis
                    memcpy(enemyPos,tp,sizeof(enemyPos));
                    const float *tv = remotePlayers[t]->getVelocity();
                    enemyPos[0] += 0.3f * tv[0];
                    enemyPos[1] += 0.3f * tv[1];
                    enemyPos[2] += 0.3f * tv[2];

                    if (enemyPos[2] < 0.0f)
                        enemyPos[2] = 0.0f;
                    float dist = TargetingUtils::getTargetDistance( pos, enemyPos );
                    if (dist <= BZDB.eval(StateDatabase::BZDB_SHOCKOUTRADIUS))
                    {
                        if (!myTank->validTeamTarget(remotePlayers[t]))
                        {
                            hasSWTarget = false;
                            t = curMaxPlayers;
                        }
                        else
                            hasSWTarget = true;
                    }
                }
            }
            if (hasSWTarget)
            {
                myTank->fireShot();
                lastShot = TimeKeeper::getTick();
            }
        }
    }
    else
    {
        TimeKeeper now = TimeKeeper::getTick();
        if (now - lastShot >= (1.0f / World::getWorld()->getMaxShots()))
        {

            float errorLimit = World::getWorld()->getMaxShots() * BZDB.eval(StateDatabase::BZDB_LOCKONANGLE) / 8.0f;
            float closeErrorLimit = errorLimit * 2.0f;

            for (int t = 0; t < curMaxPlayers; t++)
            {
                if (t != myTank->getId() && remotePlayers[t] &&
                        remotePlayers[t]->isAlive() && !remotePlayers[t]->isPaused() &&
                        !remotePlayers[t]->isNotResponding() &&
                        myTank->validTeamTarget(remotePlayers[t]))
                {

                    if (remotePlayers[t]->isPhantomZoned() && !myTank->isPhantomZoned()
                            && (myTank->getFlag() != Flags::SuperBullet))
                        continue;

                    const float *tp = remotePlayers[t]->getPosition();
                    float enemyPos[3];
                    //toss in some lag adjustment/future prediction - 300 millis
                    memcpy(enemyPos,tp,sizeof(enemyPos));
                    const float *tv = remotePlayers[t]->getVelocity();
                    enemyPos[0] += 0.3f * tv[0];
                    enemyPos[1] += 0.3f * tv[1];
                    enemyPos[2] += 0.3f * tv[2];
                    if (enemyPos[2] < 0.0f)
                        enemyPos[2] = 0.0f;

                    float dist = TargetingUtils::getTargetDistance( pos, enemyPos );

                    if ((myTank->getFlag() == Flags::GuidedMissile) || (fabs(pos[2] - enemyPos[2]) < 2.0f * BZDBCache::tankHeight))
                    {

                        float targetDiff = TargetingUtils::getTargetAngleDifference(pos, myAzimuth, enemyPos );
                        if ((targetDiff < errorLimit)
                                ||  ((dist < (2.0f * BZDB.eval(StateDatabase::BZDB_SHOTSPEED))) && (targetDiff < closeErrorLimit)))
                        {
                            bool isTargetObscured;
                            if (myTank->getFlag() != Flags::SuperBullet)
                                isTargetObscured = TargetingUtils::isLocationObscured( pos, enemyPos );
                            else
                                isTargetObscured = false;

                            if (!isTargetObscured)
                            {
                                myTank->fireShot();
                                lastShot = now;
                                t = curMaxPlayers;
                            }
                        }
                    }
                }
            }
        }
    }
}

bool Plan::avoidBullet(float &rotation, float &speed)
{
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    const float *pos = myTank->getPosition();

    if ((myTank->getFlag() == Flags::Narrow) || (myTank->getFlag() == Flags::Burrow))
        return false; // take our chances

    float minDistance;
    ShotPath *shot = findWorstBullet(minDistance);

    if ((shot == NULL) || (minDistance > 100.0f))
        return false;

    const float *shotPos = shot->getPosition();
    const float *shotVel = shot->getVelocity();
    float shotAngle = atan2f(shotVel[1],shotVel[0]);
    float shotUnitVec[2] = {cosf(shotAngle), sinf(shotAngle)};

    float trueVec[2] = {(pos[0]-shotPos[0])/minDistance,(pos[1]-shotPos[1])/minDistance};
    float dotProd = trueVec[0]*shotUnitVec[0]+trueVec[1]*shotUnitVec[1];

    if (((World::getWorld()->allowJumping() || (myTank->getFlag()) == Flags::Jumping
            || (myTank->getFlag()) == Flags::Wings))
            && (minDistance < (std::max(dotProd,0.5f) * BZDBCache::tankLength * 2.25f))
            && (myTank->getFlag() != Flags::NoJumping))
    {
        myTank->setJump();
        return (myTank->getFlag() != Flags::Wings);
    }
    else if (dotProd > 0.96f)
    {
        speed = 1.0;
        float myAzimuth = myTank->getAngle();
        float rotation1 = TargetingUtils::normalizeAngle((float)((shotAngle + M_PI/2.0) - myAzimuth));

        float rotation2 = TargetingUtils::normalizeAngle((float)((shotAngle - M_PI/2.0) - myAzimuth));

        float zCross = shotUnitVec[0]*trueVec[1] - shotUnitVec[1]*trueVec[0];

        if (zCross > 0.0f)   //if i am to the left of the shot from shooter pov
        {
            rotation = rotation1;
            if (fabs(rotation1) < fabs(rotation2))
                speed = 1.0f;
            else if (dotProd > 0.98f)
                speed = -0.5f;
            else
                speed = 0.5f;
        }
        else
        {
            rotation = rotation2;
            if (fabs(rotation2) < fabs(rotation1))
                speed = 1.0f;
            else if (dotProd > 0.98f)
                speed = -0.5f;
            else
                speed = 0.5f;
        }

        return true;
    }
    return false;
}

ShotPath *Plan::findWorstBullet(float &minDistance)
{
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    const float *pos = myTank->getPosition();
    ShotPath *minPath = NULL;

    minDistance = Infinity;
    for (int t = 0; t < curMaxPlayers; t++)
    {
        if (t == myTank->getId() || !remotePlayers[t])
            continue;

        const int maxShots = remotePlayers[t]->getMaxShots();
        for (int s = 0; s < maxShots; s++)
        {
            ShotPath* shot = remotePlayers[t]->getShot(s);
            if (!shot || shot->isExpired())
                continue;

            if ((shot->getFlag() == Flags::InvisibleBullet) &&
                    (myTank->getFlag() != Flags::Seer))
                continue; //Theoretically Roger could triangulate the sound
            if (remotePlayers[t]->isPhantomZoned() && !myTank->isPhantomZoned())
                continue;
            if ((shot->getFlag() == Flags::Laser) &&
                    (myTank->getFlag() == Flags::Cloaking))
                continue; //cloaked tanks can't die from lasers

            const float* shotPos = shot->getPosition();
            if ((fabs(shotPos[2] - pos[2]) > BZDBCache::tankHeight) &&
                    (shot->getFlag() != Flags::GuidedMissile))
                continue;

            const float dist = TargetingUtils::getTargetDistance(pos, shotPos);
            if (dist < minDistance)
            {
                const float *shotVel = shot->getVelocity();
                float shotAngle = atan2f(shotVel[1], shotVel[0]);
                float shotUnitVec[2] = {cosf(shotAngle), sinf(shotAngle)};

                float trueVec[2] = { (pos[0] - shotPos[0]) / dist, (pos[1] - shotPos[1]) / dist };
                float dotProd = trueVec[0] * shotUnitVec[0] + trueVec[1] * shotUnitVec[1];

                if (dotProd <= 0.1f) //pretty wide angle, evasive actions prolly aren't gonna work
                    continue;

                minDistance = dist;
                minPath = shot;
            }
        }
    }

    float oldDistance = minDistance;
    WorldPlayer *wp = World::getWorld()->getWorldWeapons();
    for (int w = 0; w < wp->getMaxShots(); w++)
    {
        ShotPath* shot = wp->getShot(w);
        if (!shot || shot->isExpired())
            continue;

        if (shot->getFlag() == Flags::InvisibleBullet && myTank->getFlag() != Flags::Seer)
            continue; //Theoretically Roger could triangulate the sound
        if (shot->getFlag() == Flags::Laser && myTank->getFlag() == Flags::Cloaking)
            continue; //cloaked tanks can't die from lasers

        const float* shotPos = shot->getPosition();
        if ((fabs(shotPos[2] - pos[2]) > BZDBCache::tankHeight) && (shot->getFlag() != Flags::GuidedMissile))
            continue;

        const float dist = TargetingUtils::getTargetDistance( pos, shotPos );
        if (dist < minDistance)
        {
            const float *shotVel = shot->getVelocity();
            float shotAngle = atan2f(shotVel[1], shotVel[0]);
            float shotUnitVec[2] = {cosf(shotAngle), sinf(shotAngle)};

            float trueVec[2] = { (pos[0] - shotPos[0]) / dist, (pos[1] - shotPos[1]) / dist };
            float dotProd = trueVec[0] * shotUnitVec[0] + trueVec[1] * shotUnitVec[1];

            if (dotProd <= 0.1f) //pretty wide angle, evasive actions prolly aren't gonna work
                continue;

            minDistance = dist;
            minPath = shot;
        }
    }
    if (oldDistance < minDistance)
        minDistance = oldDistance; //pick the closer bullet
    return minPath;
}


/**
 * PlanStack
 */

PlanStack::PlanStack()
{
    Plan *pPlan = new TopLevelPlan();
    plans.push(pPlan);
}

PlanStack::~PlanStack()
{
    while (plans.size() > 0)
    {
        Plan* pPlan = plans.top();
        delete pPlan;
        plans.pop();
    }
}

void PlanStack::execute(float &rotation, float &speed)
{
    if (Plan::avoidBullet(rotation, speed))
        return;

    Plan *pPlan = NULL;

    while (!plans.empty())
    {
        pPlan = plans.top();
        if (!pPlan->isValid())
        {
            delete pPlan;
            plans.pop();
        }
    }

    while (pPlan->usesSubPlan())
    {
        pPlan = pPlan->createSubPlan();
        plans.push(pPlan);
    }

    pPlan->execute(rotation, speed);
}

/**
 * TopLevelPlan
 */

TopLevelPlan::TopLevelPlan()
    : Plan(0)
{
}

bool TopLevelPlan::isValid()
{
    //always valid
    return true;
}

bool TopLevelPlan::usesSubPlan()
{
    return true;
}

Plan *TopLevelPlan::createSubPlan()
{
    //TODO: Pick a plan
    return NULL;
}


/**
 * GotoPointPlan
 */

GotoPointPlan::GotoPointPlan(float *pt)
    : Plan(20.0f)
{
    memcpy( gotoPt, pt, sizeof( gotoPt ));
}

bool GotoPointPlan::usesSubPlan()
{
    return false;
}

Plan *GotoPointPlan::createSubPlan()
{
    return NULL;
}

void GotoPointPlan::execute(float &rotation, float &speed)
{
    //TODO: goto point, then

    Plan::execute(rotation, speed);
}

/**
 * WeavePlan
 */

WeavePlan::WeavePlan(int pID, bool right )
    : Plan(10.0)
{
    playerID = pID;
    weaveRight = right;
}

bool WeavePlan::isValid()
{
    Player *pPlayer = lookupPlayer(playerID);
    if (pPlayer == NULL)
        return false;

    if (!pPlayer->isAlive())
        return false;

    LocalPlayer *myTank = LocalPlayer::getMyTank();
    const float *pVel = myTank->getVelocity();
    if ((pVel[0] == 0.0f) && (pVel[1] == 0.0f) && (pVel[2] == 0.0f))
        return false;

    return true;
}

bool WeavePlan::usesSubPlan()
{
    return false;
}

Plan* WeavePlan::createSubPlan()
{
    return NULL;
}

void WeavePlan::execute(float &rotation, float &speed)
{
    //TODO: weave, then

    Plan::execute(rotation, speed);
}

/**
 * HuntPlayerPlan
 */

HuntPlayerPlan::HuntPlayerPlan()
    :Plan(300.0f)
{
    //Pick a player ID to hunt
    playerID = 0;
}
bool HuntPlayerPlan::isValid()
{
    if (!Plan::isValid())
        return false;

    Player *pPlayer = lookupPlayer(playerID);
    if (pPlayer == NULL)
        return false;

    if (!pPlayer->isAlive())
        return false;

    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (pPlayer->getTeam() == myTank->getTeam())
        return false;

    return true;
}

bool HuntPlayerPlan::usesSubPlan()
{
    return true;
}

Plan *HuntPlayerPlan::createSubPlan()
{
    Player *pPlayer = lookupPlayer(playerID);
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    bool isObscured = TargetingUtils::isLocationObscured( myTank->getPosition(), pPlayer->getPosition());
    if (isObscured)
    {
        float pt[3];
        // fill in pt with a open spot to go to
        return new GotoPointPlan(pt);
    }
    else
        return new WeavePlan(playerID, bzfrand() > 0.5f);
}


/**
 * HuntTeamFlagPlan
 */

HuntTeamFlagPlan::HuntTeamFlagPlan() : Plan(300.0f)
{
}

bool HuntTeamFlagPlan::isValid()
{
    return false;
}

bool HuntTeamFlagPlan::usesSubPlan()
{
    return true;
}

Plan *HuntTeamFlagPlan::createSubPlan()
{
    return NULL;
}

/**
 * CaptureFlagPlan
 */

CaptureFlagPlan::CaptureFlagPlan()
    :Plan(1200.0f)
{
}

bool CaptureFlagPlan::isValid()
{
    return false;
}

bool CaptureFlagPlan::usesSubPlan()
{
    return true;
}

Plan *CaptureFlagPlan::createSubPlan()
{
    return NULL;
}

// Local Variables: ***
// mode: C++ ***
// tab-width: 4 ***
// c-basic-offset: 4 ***
// indent-tabs-mode: nil ***
// End: ***
// ex: shiftwidth=4 tabstop=4
