package com.brackeen.javagamebook.game;

import com.brackeen.javagamebook.bsp2D.*;
import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.game.GameObject;

/**
    Klasa CollisionDetection obsługuje wykrywanie wzajemnych kolizji
    obiektów klasy GameObjects, oraz kolizji tych obiektów z obiektami
    przechowywanymi w drzewie BSP. W przypadku wykrycia kolizji, obiekt
    GameObject nie zatrzymuje się, tylko przesuwa wzdłuż obiektu, z
    którym koliduje.
*/
public class CollisionDetectionWithSliding
    extends CollisionDetection
{

    private Vector3D scratch = new Vector3D();
    private Vector3D originalLocation = new Vector3D();

    /**
        Tworzy nowy obiekt klasy CollisionDetectionWithSliding
        dla przekazanego drzewa BSP.
    */
    public CollisionDetectionWithSliding(BSPTree bspTree) {
        super(bspTree);
    }


    /**
        Sprawdza, czy obiekt w grze nie koliduje ze ścianami reprezentowanymi
        w drzewie BSP. Zwraca pierwszą ścianę, dla której występuje kolizja, lub
        null, jeśli obiekt nie koliduje z żadną ścianą. Jeśli metoda wykryje
        kolizję, obiekt przemieszcza się wzdłuż ściany i sprawdza, czy nie
        występują kolejne kolizje. Jeśli w czasie ruchu wzdłuż ściany nastąpi
        nowa kolizja, obiekt jest przywracany na swoją pierwotną pozycję.
    */
    public BSPPolygon checkWalls(GameObject object, Vector3D oldLocation, long elapsedTime)
    {
    
        float goalX = object.getX();
        float goalZ = object.getZ();
    
        BSPPolygon wall = super.checkWalls(object, oldLocation, elapsedTime);
        // jeśli wykryto kolizję i obiekt się sam nie zatrzymał
        if (wall != null && object.getTransform().isMoving()) {
            float actualX = object.getX();
            float actualZ = object.getZ();
    
            // iloczyn skalarny wektora prostopadłego do ściany i linii prowadzącej do celu
            scratch.setTo(actualX, 0, actualZ);
            scratch.subtract(goalX, 0, goalZ);
            float length = scratch.getDotProduct(wall.getNormal());
    
            float slideX = goalX + length * wall.getNormal().x;
            float slideZ = goalZ + length * wall.getNormal().z;
    
            object.getLocation().setTo(slideX, object.getY(), slideZ);
            originalLocation.setTo(oldLocation);
            oldLocation.setTo(actualX, oldLocation.y, actualZ);
    
            // użyj mniejszego promienia otoczenia podczas ruchu wzdłuż ściany
            PolygonGroupBounds bounds = object.getBounds();
            float originalRadius = bounds.getRadius();
            bounds.setRadius(originalRadius-1);
    
            // sprawdź występowanie kolizji w czasie ruchu wzdłuż ściany
            BSPPolygon wall2 = super.checkWalls(object, oldLocation, elapsedTime);
    
            // przywróć zmienione parametry
            oldLocation.setTo(originalLocation);
            bounds.setRadius(originalRadius);
    
            if (wall2 != null) {
                object.getLocation().setTo(
                    actualX, object.getY(), actualZ);
                return wall2;
            }
        }
    
        return wall;
    }


    /**
        Sprawdza, czy obiekt koliduje z podłogą i sufitem.
        Używa metod object.getFloorHeight() i object.getCeilHeight()
        do pozyskania wartości definiujących podłogę i sufit. Stosuje
        przyspieszenie ziemskie dla obiektu znajdującego się w
        powietrzu i płynne przyspieszenie w górę dla gracza
        znajdującego się na wysokości mniejszej niż wynosi wysokość
        podłogi (celem uzyskania efektu płynnego wchodzenia po schodach).
    */
    protected void checkFloorAndCeiling(GameObject object, long elapsedTime)
    {
        float floorHeight = object.getFloorHeight();
        float ceilHeight = object.getCeilHeight();
        float bottomHeight = object.getBounds().getBottomHeight();
        float topHeight = object.getBounds().getTopHeight();
        Vector3D v = object.getTransform().getVelocity();
        Physics physics = Physics.getInstance();
    
        // sprawdza, czy obiekt stoi na podłodze
        if (object.getY() + bottomHeight == floorHeight) {
            if (v.y < 0) {
                v.y = 0;
            }
        }
        // sprawdza, czy obiekt nie znajduje się poniżej poziomu podłogi
        else if (object.getY() + bottomHeight < floorHeight) {
    
            if (!object.isFlying()) {
                // jeśli opada
                if (v.y < 0) {
                    object.notifyFloorCollision();
                    v.y = 0;
                    object.getLocation().y = floorHeight - bottomHeight;
                }
                else if (!object.isJumping()) {
                    physics.scootUp(object, elapsedTime);
                }
            }
            else {
                object.notifyFloorCollision();
                v.y = 0;
                object.getLocation().y = floorHeight - bottomHeight;
            }
        }
        // sprawdza, czy obiekt nie uderza w sufit
        else if (object.getY() + topHeight > ceilHeight) {
            object.notifyCeilingCollision();
            if (v.y > 0) {
                v.y = 0;
            }
            object.getLocation().y = ceilHeight - topHeight;
            if (!object.isFlying()) {
                physics.applyGravity(object, elapsedTime);
            }
        }
        // nad podłogą
        else {
            if (!object.isFlying()) {
                // zatrzymaj ruch po schodach w górę (jeśli obiekt jest w takim ruchu)
                if (v.y > 0 && !object.isJumping()) {
                    v.y = 0;
                    object.getLocation().y = floorHeight - bottomHeight;
                }
                else {
                    physics.applyGravity(object, elapsedTime);
                }
            }
        }


    }


    /**
        Obsługuje kolizję obiektu. Obiekt A jest obiektem poruszającym
        się, Obiekt B jest tym obiektem, z którym obiekt A koliduje.
        Obiekt A przesuwa się wzdłuż krawędzi obiektu B lub (jeśli to
        możliwe) przekracza ten obiekt.
    */
    protected boolean handleObjectCollision(GameObject objectA, GameObject objectB, float distSq, float minDistSq, Vector3D oldLocation)
    {
        objectA.notifyObjectCollision(objectB);
    
        if (objectA.isFlying()) {
            return true;
        }
    
        float stepSize = objectA.getBounds().getTopHeight() / 6;
        Vector3D velocity = objectA.getTransform().getVelocity();
    
        // jeśli to możliwe, stań na obiekcie statycznym
        float objectABottom = objectA.getY() + objectA.getBounds().getBottomHeight();
        float objectBTop = objectB.getY() + objectB.getBounds().getTopHeight();
        if (objectABottom + stepSize > objectBTop && objectBTop + objectA.getBounds().getTopHeight() < objectA.getCeilHeight())
        {
            objectA.getLocation().y = (objectBTop - objectA.getBounds().getBottomHeight());
            if (velocity.y < 0) {
                objectA.setJumping(false);
                // uwzględnij działanie siły grawitacji
                velocity.y = -.01f;
            }
            return false;
        }
    
        if (objectA.getX() != oldLocation.x || objectA.getZ() != oldLocation.z)
        {
            // przesuwaj obiekt wzdłuż krawędzi obiektu statycznego
            float slideDistFactor = (float)Math.sqrt(minDistSq / distSq) - 1;
            scratch.setTo(objectA.getX(), 0, objectA.getZ());
            scratch.subtract(objectB.getX(), 0, objectB.getZ());
            scratch.multiply(slideDistFactor);
            objectA.getLocation().add(scratch);
    
            // jeśli obiekt koliduje ze ścianą, przywróć go do pierwotnego położenia
            if (super.checkWalls(objectA, oldLocation, 0) != null)
            {
                return true;
            }
    
            return false;
        }
    
        return true;
    }



}
