package com.brackeen.javagamebook.ai;

import com.brackeen.javagamebook.ai.pattern.AimPattern;
import com.brackeen.javagamebook.path.PathFinder;
import com.brackeen.javagamebook.path.PathBot;
import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.game.*;
import com.brackeen.javagamebook.util.MoreMath;
import com.brackeen.javagamebook.shooter3D.MessageQueue;

public class AIBot extends PathBot {

    public static final int NORMAL_STATE_IDLE = 0;
    public static final int NORMAL_STATE_PATROL = 1;
    public static final int NORMAL_STATE_CHASE = 2;
    public static final int BATTLE_STATE_ATTACK = 3;
    public static final int BATTLE_STATE_DODGE = 4;
    public static final int BATTLE_STATE_RUN_AWAY = 5;
    public static final int WOUNDED_STATE_HURT = 6;
    public static final int WOUNDED_STATE_DEAD = 7;

    public static final int DECESION_READY = 9;

    private static final float DEFAULT_MAX_HEALTH = 100;
    private static final float CRITICAL_HEALTH_PERCENT = 5;

    private float maxHealth;
    private float health;
    private int aiState;
    private long elapsedTimeInState;
    private long elapsedTimeSinceDecision;
    private long timeSincePlayerLastSeen;
    private Vector3D startLocation;
    private boolean isRegenerating;

    private PolygonGroup blastModel;
    private CollisionDetection collisionDetection;
    protected Brain brain;

    // wyłącznie do wyświetlania komunikatów testowych w grze
    private boolean lastVisible;

    public AIBot(PolygonGroup polygonGroup,
        CollisionDetection collisionDetection, Brain brain,
        PolygonGroup blastModel)
    {
        super(polygonGroup);
        this.collisionDetection = collisionDetection;
        this.brain = brain;
        this.blastModel = blastModel;

        // losowy czas przed posjęciem decyzji
        elapsedTimeSinceDecision =
            MoreMath.random((int)brain.decisionTime);
        maxHealth = DEFAULT_MAX_HEALTH;
        setHealth(maxHealth);
        aiState = NORMAL_STATE_IDLE;
        timeSincePlayerLastSeen = 10000;
    }


    public float getHealth() {
        return health;
    }

    public float getMaxHealth() {
        return maxHealth;
    }

    protected void setHealth(float health) {
        this.health = health;
    }


    /**
        Dodaje określoną liczbę punktów do poziomu zdrowia bota.
        Jeśli ta liczba jest ujemna, stan bota jest ustawiany na
        WOUNDED_STATE_HURT.
    */
    public void addHealth(float amount) {
        if (amount < 0) {
            if (health <= 0 || aiState == WOUNDED_STATE_HURT) {
                return;
            }
            MessageQueue.getInstance().debug(getName() + " hit");
            setAiState(WOUNDED_STATE_HURT, null);
            // podejmij decyzję w ciągu trzech sekund
            elapsedTimeSinceDecision = brain.decisionTime - 3000;
        }
        setHealth(health + amount);
    }


    /**
        Zwraca true, jeśli poziom zdrowia bota jest krytyczny
        (mniejszy niż stała CRITICAL_HEALTH_PERCENT).
    */
    public boolean isCriticalHealth() {
        return (health / maxHealth < CRITICAL_HEALTH_PERCENT/100);
    }

    /**
        Zwraca stan sztucznej inteligencji dla tego bota (inny niż
        stan obiektu GameObject).
    */
    public int getAiState() {
        return aiState;
    }


    /**
        Ustawia stan sztucznej inteligencji dla tego bota (inny niż
        stan obiektu GameObject).
    */
    protected void setAiState(int aiState, GameObject player) {
        if (this.aiState == aiState) {
            return;
        }

        this.aiState = aiState;

        elapsedTimeInState = 0;
        PathFinder lastPattern = pathFinder;
        Vector3D playerLocation = null;
        if (player != null) {
            playerLocation = player.getLocation();
        }

        // aktualizuje drogę
        switch (aiState) {
            case NORMAL_STATE_IDLE:
            case NORMAL_STATE_PATROL:
                setPathFinder(brain.idlePathFinder);
                setFacing(null);
                break;
            case NORMAL_STATE_CHASE:
                setPathFinder(brain.chasePathFinder);
                setFacing(null);
                break;
            case BATTLE_STATE_ATTACK:
                setPathFinder(brain.attackPathFinder);
                setFacing(playerLocation);
                break;
            case BATTLE_STATE_DODGE:
                setPathFinder(brain.dodgePathFinder);
                setFacing(null);
                break;
            case BATTLE_STATE_RUN_AWAY:
                setPathFinder(brain.runAwayPathFinder);
                setFacing(null);
                break;
            case WOUNDED_STATE_HURT:
                setPathFinder(null);
                setFacing(null);
                getTransform().stop();
                getTransform().setAngleVelocityY(
                    MoreMath.random(0.001f, 0.05f),
                    MoreMath.random(100, 500));
                break;
            case WOUNDED_STATE_DEAD:
                setPathFinder(null);
                setFacing(null);
                setJumping(true);
                getTransform().stop();
                Physics.getInstance().jumpToHeight(this, 16);
                getTransform().setAngleVelocityY(
                    MoreMath.random(0.001f, 0.05f),
                    MoreMath.random(100, 500));
                break;
            default:
                setPathFinder(null);
                setFacing(null);
                getTransform().stop();
                break;
        }

        if (lastPattern != pathFinder) {
            MessageQueue.getInstance().debug(
                getName() + " pattern: " + pathFinder);
        }
    }


    /**
        Zwraca true, jeśli ten bot jest wskrzeszany po swojej śmierci.
    */
    public boolean isRegenerating() {
        return isRegenerating;
    }


    /**
        Ustawia wskrzeszanie tego bota po jego śmierci.
    */
    public void setRegenerating(boolean isRegenerating) {
        this.isRegenerating = isRegenerating;
    }


    /**
        Ustawia klasę PathFinder wykorzystywaną do ruchu po drodze.
    */
    public void setPathFinder(PathFinder pathFinder) {
        if (this.pathFinder != pathFinder) {
            super.setPathFinder(pathFinder);
            timeUntilPathRecalc = 0;
        }
    }

    /**
        Powoduje, że ten bot jest wskrzeszany - odtwarza jego
        oryginalne położenie.
    */
    protected void regenerate() {
        setHealth(maxHealth);
        setState(STATE_ACTIVE);
        setAiState(DECESION_READY, null);
        getLocation().setTo(startLocation);
        getTransform().stop();
        setJumping(false);
        setPathFinder(null);
        setFacing(null);
        lastVisible = false;
        timeSincePlayerLastSeen = 10000;
        // informuje menadżera obiektów o wskrzeszeniu tego obiektu
        // (dzięki temu nie będą wykrywane kolizje na drodze od
        // dotychczasowego do nowego położenia)
        addSpawn(this);
    }

    public void update(GameObject player, long elapsedTime) {
        updateHelper(player, elapsedTime);
        super.update(player, elapsedTime);
    }

    public void updateHelper(GameObject player, long elapsedTime) {

        elapsedTimeSinceDecision+=elapsedTime;
        elapsedTimeInState+=elapsedTime;
        timeSincePlayerLastSeen+=elapsedTime;

        // zapisz początkowe położenie
        if (startLocation == null) {
            startLocation = new Vector3D(getLocation());
        }

        // wskrześ martwego bota po pięciu sekundach
        if (aiState == WOUNDED_STATE_DEAD) {
            if (elapsedTimeInState >= 5000) {
                if (isRegenerating()) {
                    regenerate();
                }
                else {
                    setState(STATE_DESTROYED);
                }
            }
            return;
        }

        else if (aiState == WOUNDED_STATE_HURT) {
            if (elapsedTimeInState >= 500) {
                if (health <= 0) {
                    setAiState(WOUNDED_STATE_DEAD, player);
                    return;
                }
                else {
                    aiState = DECESION_READY;
                }
            }
            else {
                return;
            }
        }


        // uciekaj, jeśli poziom zdrowia jest krytyczny
        if (isCriticalHealth() &&
            brain.runAwayPathFinder != null)
        {
            setAiState(BATTLE_STATE_RUN_AWAY, player);
            return;
        }


        // jeśli bezczynny i widzi gracza, podejmuje decyzje co 500 ms
        if ((aiState == NORMAL_STATE_IDLE ||
            aiState == NORMAL_STATE_PATROL) &&
            elapsedTimeInState >= 500)
        {
            aiState = DECESION_READY;
        }

        // jeśli czas minął, podejmij decyzję
        else if (elapsedTimeSinceDecision >= brain.decisionTime) {
            aiState = DECESION_READY;
        }

        // jeśli bot przeszedł aktualną drogę, podejmij decyzję
        else if (currentPath != null && !currentPath.hasNext() &&
            !getTransform().isMovingIgnoreY())
        {
            aiState = DECESION_READY;
        }

        // podejmij nową decyzję
        if (aiState == DECESION_READY) {
            elapsedTimeSinceDecision = 0;

            if (canSee(player)) {
                setAiState(chooseBattleState(), player);
            }
            else if (timeSincePlayerLastSeen < 3000 ||
                canHear(player))
            {
                setAiState(NORMAL_STATE_CHASE, player);
            }
            else {
                setAiState(NORMAL_STATE_IDLE, player);
            }
        }
        // wystrzel pocisk
        else if (aiState == BATTLE_STATE_ATTACK &&
            elapsedTimeInState >= brain.aimTime &&
            brain.aimPathFinder != null)
        {
            elapsedTimeInState-=brain.aimTime;

            // dłuższy czas celowania == większa celność
            float p = Math.min(1, brain.aimTime / 2000f);
            ((AimPattern)brain.aimPathFinder).setAccuracy(p);
            Vector3D direction = (Vector3D)
                brain.aimPathFinder.find(this, player).next();
            fireProjectile(direction);
        }

    }

    /**
        Wystrzeliwuje pocisk w danym kierunku. Wektor kierunku
        powinien być znormalizowany.
    */
    public void fireProjectile(Vector3D direction) {

        Projectile blast = new Projectile(
            (PolygonGroup)blastModel.clone(),
            direction, this, 3, 6);
        float dist = 2 * (getBounds().getRadius() +
            blast.getBounds().getRadius());
        blast.getLocation().setTo(
            getX() + direction.x*dist,
            getY() + getBounds().getTopHeight()/2,
            getZ() + direction.z*dist);

        // "tworzy" nowy obiekt w grze
        addSpawn(blast);

        // powoduje "wirtualny" hałas, który może być "usłyszany"
        // przez bota (hałas utrzymuje się przez 500 milisekund)
        makeNoise(500);
    }


    public int chooseBattleState() {
        float p = (float)Math.random();
        if (p <= brain.attackProbability) {
            return BATTLE_STATE_ATTACK;
        }
        else if (p <= brain.attackProbability +
            brain.dodgeProbability)
        {
            return BATTLE_STATE_DODGE;
        }
        else {
            return BATTLE_STATE_RUN_AWAY;
        }
    }

    /**
        Sprawdza, czy ten obiekt może zobaczyć przekazany obiekt,
        (zakładając, że ma oczy z tyłu głowy).
    */
    public boolean canSee(GameObject object) {
        // sprawdza, czy odcinek łączący tego bota z obiektem
        // nie przecina żadnych ścian
        boolean visible = (collisionDetection.getFirstWallIntersection(getX(), getZ(), object.getX(), object.getZ(), getY(), getY() + 1) == null);

        if (visible) {
            timeSincePlayerLastSeen = 0;
        }

        // wyświetla komunikaty o wyniku testu
        if (visible != lastVisible) {
            String message = visible?" widzi ":" już nie widzi ";
            MessageQueue.getInstance().debug(getName() + message + object.getName());
            lastVisible = visible;
        }

        return visible;
    }


    /**
        Sprawdza, czy ten obiekt może usłyszeć przekazany obiekt.
        Przekazany obiekt musi powodować hałas i znajdować się w
        zasięgu słuchu tego obiektu.
    */
    public boolean canHear(GameObject object) {

        // sprawdza, czy obiekt powoduje hałas i czy bot nie jest głuchy
        if (!object.isMakingNoise() || brain.hearDistance == 0) {
            return false;
        }

        // sprawdza, czy ten bot znajduje się wystarczająco blisko, by
        // usłyszeć ten hałas
        float distSq = getLocation().getDistanceSq(object.getLocation());
        float hearDistSq = brain.hearDistance * brain.hearDistance;
        boolean heard = (distSq <= hearDistSq);

        // wyświetla komunikaty o wyniku testu
        if (heard) {
            MessageQueue.getInstance().debug(getName() + " słyszy " + object.getName());
        }

        return heard;
    }

    public boolean isFlying() {
        return (super.isFlying() && aiState != WOUNDED_STATE_DEAD);
    }

    public void notifyEndOfPath() {
        if (aiState != BATTLE_STATE_ATTACK) {
            setAiState(DECESION_READY, null);
        }
    }

    public void notifyHitPlayer(long damage) {
        // nic nie rób
    }

   public void notifyWallCollision() {
        if (aiState == BATTLE_STATE_RUN_AWAY) {
            getTransform().setVelocity(new Vector3D(0,0,0));
            setAiState(DECESION_READY, null);
        }
        else {
            super.notifyWallCollision();
        }
   }
}
