package com.brackeen.javagamebook.math3D;

/**
    Klasa MovingTransform3D jest klasą potomną klasy Transform3D
    i obsługuje ruch postępowy i ruch obrotowy wokół osi x, y i z.
*/
public class MovingTransform3D extends Transform3D {

    public static final int FOREVER = -1;

    // Vector3D używany do obliczeń
    private static Vector3D temp = new Vector3D();

    // szybkość (w jednostkach na milisekundę)
    private Vector3D velocity;
    private Movement velocityMovement;

    // prędkość ruchu obrotowego (w radianach na milisekundę)
    private Movement velocityAngleX;
    private Movement velocityAngleY;
    private Movement velocityAngleZ;

    /**
        Tworzy nowy obiekt MovingTransform3D
    */
    public MovingTransform3D() {
        init();
    }


    /**
        Tworzy nowy obiekt MovingTransform3D w oparciu o te same
        wartości, które określono dla Transform3D.
    */
    public MovingTransform3D(Transform3D v) {
        super(v);
        init();
    }


    protected void init() {
        velocity = new Vector3D(0,0,0);
        velocityMovement = new Movement();
        velocityAngleX = new Movement();
        velocityAngleY = new Movement();
        velocityAngleZ = new Movement();
    }


    public Object clone() {
        return new MovingTransform3D(this);
    }


    /**
        Aktualizuje obiekt Transform3D w oparciu o przekazany czas ruchu,
        który upłynął. Aktualizowane jest położenie i kąty obiektu.
    */
    public void update(long elapsedTime) {
        float delta = velocityMovement.getDistance(elapsedTime);
        if (delta != 0) {
            temp.setTo(velocity);
            temp.multiply(delta);
            location.add(temp);
        }

        rotateAngle(
            velocityAngleX.getDistance(elapsedTime),
            velocityAngleY.getDistance(elapsedTime),
            velocityAngleZ.getDistance(elapsedTime));
    }


    /**
        Zatrzymuje obiekt Transform3D. Wartości wszystkich wektorów ruchu
        są zerowane.
    */
    public void stop() {
        velocity.setTo(0,0,0);
        velocityMovement.set(0,0);
        velocityAngleX.set(0,0);
        velocityAngleY.set(0,0);
        velocityAngleZ.set(0,0);
    }


    /**
        Ustawia wartości dla ruchu do określonego miejsca
        z określoną szybkością.
    */
    public void moveTo(Vector3D destination, float speed) {
        temp.setTo(destination);
        temp.subtract(location);

        // oblicza czas potrzebny na przebycie wyznaczonej 
        // odległości
        float distance = temp.length();
        long time = (long)(distance / speed);

        // normalizuje wektor kierunku
        temp.divide(distance);
        temp.multiply(speed);

        setVelocity(temp, time);
    }


    /**
        Zwraca true, jeśli obiekt jest w ruchu.
    */
    public boolean isMoving() {
        return !velocityMovement.isStopped() && !velocity.equals(0,0,0);
    }


    /**
        Returns true if currently moving, ignoring the y movement.
    */
    public boolean isMovingIgnoreY() {
        return !velocityMovement.isStopped() &&
            (velocity.x != 0 || velocity.z != 0);
    }


    /**
        Zwraca pozostały czas, w którym obiekt będzie w ruchu.
    */
    public long getRemainingMoveTime() {
        if (!isMoving()) {
            return 0;
        }
        else {
            return velocityMovement.remainingTime;
        }
    }


    /**
        Zwraca wektor ruchu. Jeśli wektor jest bezpośrednio
        modyfikowany, wywołaj metodę setVelocity(), aby upewnić
        się, że zmiany zostaną uwzględnione.
    */
    public Vector3D getVelocity() {
        return velocity;
    }


    /**
        Ustawia wartość szybkości dla danego wektora.
    */
    public void setVelocity(Vector3D v) {
        setVelocity(v, FOREVER);
    }


    /**
        Ustawia szybkość. Wartość ta jest automatycznie zerowana
        w momencie, gdy upłynie przekazana ilość czasu (liczba
        milisekund). Jeśli przekazano wartość FOREVER, szybkość
        nigdy nie zostanie zredukowana do zera.
    */
    public void setVelocity(Vector3D v, long time) {
        if (velocity != v) {
            velocity.setTo(v);
        }
        if (v.x == 0 && v.y == 0 && v.z == 0) {
            velocityMovement.set(0, 0);
        }
        else {
            velocityMovement.set(1, time);
        }

    }


    /**
        Dodaje przekazaną wartość do aktualnej szybkości ruchu.
        Jeśli ten obiekt MovingTransform3D jest aktualnie w ruchu,
        jego czas pozostały do zatrzymania nie ulegnie zmianie.
        W przeciwnym przypadku, pozostały czas będzie miał wartość
        FOREVER.
    */
    public void addVelocity(Vector3D v) {
        if (isMoving()) {
            velocity.add(v);
        }
        else {
            setVelocity(v);
        }
    }


    /**
        Obraca obiekt wokół osi x do danego kąta z określoną
        prędkością.
    */
    public void turnXTo(float angleDest, float speed) {
        turnTo(velocityAngleX, getAngleX(), angleDest, speed);
    }


    /**
        Obraca obiekt wokół osi y do danego kąta z określoną
        prędkością.
    */
    public void turnYTo(float angleDest, float speed) {
        turnTo(velocityAngleY, getAngleY(), angleDest, speed);
    }


    /**
        Obraca obiekt wokół osi z do danego kąta z określoną
        prędkością.
    */
    public void turnZTo(float angleDest, float speed) {
        turnTo(velocityAngleZ, getAngleZ(), angleDest, speed);
    }

    /**
        Obraca z określoną prędkością obiekt wokół osi x aż 
        przyjmie pozycję przodem do kierunku danego wektora
        (y, z).
    */
    public void turnXTo(float y, float z, float angleOffset,
        float speed)
    {
        turnXTo((float)Math.atan2(-z,y) + angleOffset, speed);
    }


    /**
        Obraca z określoną prędkością obiekt wokół osi y aż 
        przyjmie pozycję przodem do kierunku danego wektora
        (x, z).
    */
    public void turnYTo(float x, float z, float angleOffset,
        float speed)
    {
        turnYTo((float)Math.atan2(-z,x) + angleOffset, speed);
    }


    /**
        Obraca z określoną prędkością obiekt wokół osi z aż 
        przyjmie pozycję przodem do kierunku danego wektora
        (x, y).
    */
    public void turnZTo(float x, float y, float angleOffset,
        float speed)
    {
        turnZTo((float)Math.atan2(y,x) + angleOffset, speed);
    }


    /**
        Upewnia się, że dany kąt należy do przedziału od -pi do pi.
        Zwraca ten kąt lub wartość poprawioną, jeśli nie mieścił się
        w prawidłowym przedziale.
    */
    protected float ensureAngleWithinBounds(float angle) {
        if (angle < -Math.PI || angle > Math.PI) {
            // przekształca przedział do postaci od 0 do 1
            double newAngle = (angle + Math.PI) / (2*Math.PI);
            // poprawia przedział
            newAngle = newAngle - Math.floor(newAngle);
            // przekształca przedział ponownie do postaci od -pi do pi
            newAngle = Math.PI * (newAngle * 2 - 1);
            return (float)newAngle;
        }
        return angle;
    }


    /**
        Zmienia z określoną szybkością kąt ruchu od kąta startAngle
        do kąta endAngle.
    */
    protected void turnTo(Movement movement,
        float startAngle, float endAngle, float speed)
    {
        startAngle = ensureAngleWithinBounds(startAngle);
        endAngle = ensureAngleWithinBounds(endAngle);
        if (startAngle == endAngle) {
            movement.set(0,0);
        }
        else {

            float distanceLeft;
            float distanceRight;
            float pi2 = (float)(2*Math.PI);

            if (startAngle < endAngle) {
                distanceLeft = startAngle - endAngle + pi2;
                distanceRight = endAngle - startAngle;
            }
            else {
                distanceLeft = startAngle - endAngle;
                distanceRight = endAngle - startAngle + pi2;
            }

            if (distanceLeft < distanceRight) {
                speed = -Math.abs(speed);
                movement.set(speed, (long)(distanceLeft / -speed));
            }
            else {
                speed = Math.abs(speed);
                movement.set(speed, (long)(distanceRight / speed));
            }
        }
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi x.
    */
    public void setAngleVelocityX(float speed) {
        setAngleVelocityX(speed, FOREVER);
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi y.
    */
    public void setAngleVelocityY(float speed) {
        setAngleVelocityY(speed, FOREVER);
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi z.
    */
    public void setAngleVelocityZ(float speed) {
        setAngleVelocityZ(speed, FOREVER);
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi x
        na określony czas.
    */
    public void setAngleVelocityX(float speed, long time) {
        velocityAngleX.set(speed, time);
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi y
        na określony czas.
    */
    public void setAngleVelocityY(float speed, long time) {
        velocityAngleY.set(speed, time);
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi z
        na określony czas.
    */
    public void setAngleVelocityZ(float speed, long time) {
        velocityAngleZ.set(speed, time);
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi x
        na określony czas.
    */
    public float getAngleVelocityX() {
        return isTurningX()?velocityAngleX.speed:0;
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi y
        na określony czas.
    */
    public float getAngleVelocityY() {
        return isTurningY()?velocityAngleY.speed:0;
    }


    /**
        Ustawia szybkość ruchu obrotowego wokół osi z
        na określony czas.
    */
    public float getAngleVelocityZ() {
        return isTurningZ()?velocityAngleZ.speed:0;
    }


    /**
        Zwraca true, jeśli obiekt aktualnie obraca się
        wokół osi x.
    */
    public boolean isTurningX() {
        return !velocityAngleX.isStopped();
    }


    /**
        Zwraca true, jeśli obiekt aktualnie obraca się
        wokół osi y.
    */
    public boolean isTurningY() {
        return !velocityAngleY.isStopped();
    }


    /**
        Zwraca true, jeśli obiekt aktualnie obraca się
        wokół osi z.
    */
    public boolean isTurningZ() {
        return !velocityAngleZ.isStopped();
    }

    /**
        Klasa Movement przechowuje szybkość oraz czas, przez jaki
        obiekt ma się poruszać z tą szybkością.
    */
    protected static class Movement {
        // zmiana na milisekundę
        float speed;
        long remainingTime;

        /**
            Ustawia właściwości ruchu zgodnie z przekazaną szybkością 
            i ilością czasu (w milisekundach).
        */
        public void set(float speed, long time) {
            this.speed = speed;
            this.remainingTime = time;
        }


        public boolean isStopped() {
            return (speed == 0) || (remainingTime == 0);
        }

        /**
            Zwraca odległość przebytą w określonym czasie (liczbie milisekund).
        */
        public float getDistance(long elapsedTime) {
            if (remainingTime == 0) {
                return 0;
            }
            else if (remainingTime != FOREVER) {
                elapsedTime = Math.min(elapsedTime, remainingTime);
                remainingTime-=elapsedTime;
            }
            return speed * elapsedTime;
        }
    }
}

