const float initialAggression = 0.0020;
const float initialPrediction = 30;
const float initialAimSpeed = 10;
const float deltaAggression = 0.000025;
const float deltaPrediction = -0.15;
const float deltaAimSpeed = 0.30;
const float maxAggression = 0.01;
const float maxPrediction = 20;
const float maxAimSpeed = 40;

float aiAggression = initialAggression;
float aiPrediction = initialPrediction;
float aiAimSpeed = initialAimSpeed;

void ResetAI()
{
    aiAggression = initialAggression;
    aiPrediction = initialPrediction;
    aiAimSpeed = initialAimSpeed;
}

void MakeAIHarder()
{
    aiAggression += deltaAggression;
    if (aiAggression > maxAggression)
        aiAggression = maxAggression;

    aiPrediction += deltaPrediction;
    if (aiPrediction < maxPrediction)
        aiPrediction = maxPrediction;

    aiAimSpeed += deltaAimSpeed;
    if (aiAimSpeed > maxAimSpeed)
        aiAimSpeed = maxAimSpeed;
}

class AIController
{
    // Use a weak handle instead of a normal handle to point to the current target
    // so that we don't mistakenly keep it alive.
    WeakHandle currentTarget;
    float newTargetTimer = 0;

    void Control(Ninja@ ownNinja, Node@ ownNode, float timeStep)
    {
        // Get new target if none. Do not constantly scan for new targets to conserve CPU time
        if (currentTarget.Get() is null)
        {
            newTargetTimer += timeStep;
            if (newTargetTimer > 0.5)
                GetNewTarget(ownNode);
        }

        Node@ targetNode = currentTarget.Get();

        if (targetNode !is null)
        {
            // Check that current target is still alive. Otherwise choose new
            Ninja@ targetNinja = cast<Ninja>(targetNode.scriptObject);
            if (targetNinja is null || targetNinja.health <= 0)
            {
                currentTarget = null;
                return;
            }

            RigidBody@ targetBody = targetNode.GetComponent("RigidBody");

            ownNinja.controls.Set(CTRL_FIRE, false);
            ownNinja.controls.Set(CTRL_JUMP, false);

            float deltaX = 0.0f;
            float deltaY = 0.0f;

            // Aim from own head to target's feet
            Vector3 ownPos(ownNode.position + Vector3(0, 0.9, 0));
            Vector3 targetPos(targetNode.position + Vector3(0, -0.9, 0));
            float distance = (targetPos - ownPos).length;

            // Use prediction according to target distance & estimated snowball speed
            Vector3 currentAim(ownNinja.GetAim() * Vector3(0, 0, 1));
            float predictDistance = distance;
            if (predictDistance > 50) predictDistance = 50;
            Vector3 predictedPos = targetPos + targetBody.linearVelocity * predictDistance / aiPrediction;
            Vector3 targetAim = (predictedPos - ownPos);

            // Add distance/height compensation
            float compensation = Max(targetAim.length - 15, 0.0);
            targetAim += Vector3(0, 0.6, 0) * compensation;

            // X-aiming
            targetAim.Normalize();
            Vector3 currentYaw(currentAim.x, 0, currentAim.z);
            Vector3 targetYaw(targetAim.x, 0, targetAim.z);
            currentYaw.Normalize();
            targetYaw.Normalize();
            deltaX = Clamp(Quaternion(currentYaw, targetYaw).yaw, -aiAimSpeed, aiAimSpeed);

            // Y-aiming
            Vector3 currentPitch(0, currentAim.y, 1);
            Vector3 targetPitch(0, targetAim.y, 1);
            currentPitch.Normalize();
            targetPitch.Normalize();
            deltaY = Clamp(Quaternion(currentPitch, targetPitch).pitch, -aiAimSpeed, aiAimSpeed);

            ownNinja.controls.yaw += 0.1 * deltaX;
            ownNinja.controls.pitch += 0.1 * deltaY;

            // Firing? if close enough and relatively correct aim
            if ((distance < 25) && (currentAim.DotProduct(targetAim) > 0.75))
            {
                if (Random(1.0) < aiAggression)
                    ownNinja.controls.Set(CTRL_FIRE, true);
            }

            // Movement
            ownNinja.dirChangeTime -= timeStep;
            if (ownNinja.dirChangeTime <= 0)
            {
                ownNinja.dirChangeTime = 0.5 + Random(1.0);
                ownNinja.controls.Set(CTRL_UP|CTRL_DOWN|CTRL_LEFT|CTRL_RIGHT, false);

                // Far distance: go forward
                if (distance > 30)
                    ownNinja.controls.Set(CTRL_UP, true);
                else if (distance > 6)
                {
                    // Medium distance: random strafing, predominantly forward
                    float v = Random(1.0);
                    if (v < 0.8)
                        ownNinja.controls.Set(CTRL_UP, true);
                    float h = Random(1.0);
                    if (h < 0.3)
                        ownNinja.controls.Set(CTRL_LEFT, true);
                    if (h > 0.7)
                        ownNinja.controls.Set(CTRL_RIGHT, true);
                }
                else
                {
                    // Close distance: random strafing backwards
                    float v = Random(1.0);
                    if (v < 0.8)
                        ownNinja.controls.Set(CTRL_DOWN, true);
                    float h = Random(1.0);
                    if (h < 0.4)
                        ownNinja.controls.Set(CTRL_LEFT, true);
                    if (h > 0.6)
                        ownNinja.controls.Set(CTRL_RIGHT, true);
                }
            }

            // Random jump, if going forward
            if ((ownNinja.controls.IsDown(CTRL_UP)) && (distance < 1000))
            {
                if (Random(1.0) < (aiAggression / 5.0))
                    ownNinja.controls.Set(CTRL_JUMP, true);
            }
        }
        else
        {
            // If no target, walk idly
            ownNinja.controls.Set(CTRL_ALL, false);
            ownNinja.controls.Set(CTRL_UP, true);
            ownNinja.dirChangeTime -= timeStep;
            if (ownNinja.dirChangeTime <= 0)
            {
                ownNinja.dirChangeTime = 1 + Random(2);
                ownNinja.controls.yaw += 0.1 * (Random(600) - 300);
            }
            if (ownNinja.isSliding)
                ownNinja.controls.yaw += 0.2;
        }
    }

    void GetNewTarget(Node@ ownNode)
    {
        newTargetTimer = 0;

        Array<Node@> nodes = ownNode.scene.GetChildrenWithScript("Ninja", true);
        float closestDistance = M_INFINITY;
        for (uint i = 0; i < nodes.length; ++i)
        {
            Node@ otherNode = nodes[i];
            Ninja@ otherNinja = cast<Ninja>(otherNode.scriptObject);
            if (otherNinja.side == SIDE_PLAYER && otherNinja.health > 0)
            {
                float distance = (ownNode.position - otherNode.position).lengthSquared;
                if (distance < closestDistance)
                {
                    currentTarget = otherNode;
                    closestDistance = distance;
                }
            }
        }
    }
}
