package com.brackeen.javagamebook.game;

import java.awt.geom.Point2D;
import java.util.List;
import com.brackeen.javagamebook.bsp2D.*;
import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.util.MoreMath;


/**
    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 zatrzymuj się.
*/
public class CollisionDetection {

    /**
        Wierzchołki otaczające obiekt w grze są wykorzystywane do
        wykrywania kolizji z poziomymi wielokątami (podłogami i
        sufitami) przechowywanymi w drzewie BSP. Wierzchołki są
        uporządkowane albo w kolejności zgodnej z kierunkiem ruchu
        wskazówek zegara, albo zgodnej z odwrotnym kierunkiem.
    */
    private static final Point2D.Float[] CORNERS = {
        new Point2D.Float(-1, -1), new Point2D.Float(-1, 1),
        new Point2D.Float(1, 1), new Point2D.Float(1, -1),
    };

    private BSPTree bspTree;
    private BSPLine path;
    private Point2D.Float intersection;

    /**
        Tworzy nowy obiekt klasy CollisionDetection dla przekazanego
        drzewa BSP.
    */
    public CollisionDetection(BSPTree bspTree) {
        this.bspTree = bspTree;
        path = new BSPLine();
        intersection = new Point2D.Float();
    }


    /**
        Sprawdza, czy obiekt GameObject koliduje ze ścianą w drzewie BSP.
        Jeśli tak, metoda zwraca wartość true.
    */
    public boolean checkBSP(GameObject object,
        Vector3D oldLocation, long elapsedTime)
    {

        boolean wallCollision = false;

        // sprawdź ściany, jeśli zmieniła się współrzędna x lub z
        if (object.getX() != oldLocation.x ||
            object.getZ() != oldLocation.z)
        {
            wallCollision = (checkWalls(object, oldLocation,
                elapsedTime) != null);
        }

        getFloorAndCeiling(object);
        checkFloorAndCeiling(object, elapsedTime);

        return wallCollision;
    }


    /**
        Zwraca wartości opisujące podłogę i sufit dla danego obiektu
        w grze (obiektu klasy GameObject). Wywołuje metody 
        object.setFloorHeight() i object.setCeilHeight() w celu
        ustawienia odpowiednich wartości dla podłogi i sufitu.
    */
    public void getFloorAndCeiling(GameObject object) {
        float x = object.getX();
        float z = object.getZ();
        float r = object.getBounds().getRadius() - 1;
        float floorHeight = Float.MIN_VALUE;
        float ceilHeight = Float.MAX_VALUE;
        BSPTree.Leaf leaf = bspTree.getLeaf(x, z);
        if (leaf != null) {
            floorHeight = leaf.floorHeight;
            ceilHeight = leaf.ceilHeight;
        }

        // sprawdza cztery otaczające punkty
        for (int i=0; i<CORNERS.length; i++) {
            float xOffset = r * CORNERS[i].x;
            float zOffset = r * CORNERS[i].y;
            leaf = bspTree.getLeaf(x + xOffset, z + zOffset);
            if (leaf != null) {
                floorHeight = Math.max(floorHeight,
                    leaf.floorHeight);
                ceilHeight = Math.min(ceilHeight,
                    leaf.ceilHeight);
            }
        }

        object.setFloorHeight(floorHeight);
        object.setCeilHeight(ceilHeight);
    }


    /**
        Sprawdza, czy obiekt koliduje z podłogą i sufitem.
        Wykorzystuje metody object.getFloorHeight() i object.getCeilHeight()
        do otrzymania wartości opisujących podłogę i sufit.
    */
    protected void checkFloorAndCeiling(GameObject object,
        long elapsedTime)
    {
        boolean collision = false;

        float floorHeight = object.getFloorHeight();
        float ceilHeight = object.getCeilHeight();
        float bottomHeight = object.getBounds().getBottomHeight();
        float topHeight = object.getBounds().getTopHeight();

        if (!object.isFlying()) {
             object.getLocation().y = floorHeight - bottomHeight;
        }
        // sprawdza, czy część obiektu nie znajduje się poniżej podłogi
        if (object.getY() + bottomHeight < floorHeight) {
            object.notifyFloorCollision();
            object.getTransform().getVelocity().y = 0;
            object.getLocation().y = floorHeight - bottomHeight;
        }
        // sprawdza, czy część obiektu nie znajduje się powyżej sufitu
        else if (object.getY() + topHeight > ceilHeight) {
            object.notifyCeilingCollision();
            object.getTransform().getVelocity().y = 0;
            object.getLocation().y = ceilHeight - topHeight;
        }

    }


    /**
        Sprawdza, czy występuje kolizja obiektu w grze ze ścianami
        reprezentowanymi w drzewie BSP. Zwraca pierwszą ścianę, z którą
        obiekt koliduje, lub null, jeśli kolizja nie występuje.
    */
    public BSPPolygon checkWalls(GameObject object,
        Vector3D oldLocation, long elapsedTime)
    {
        Vector3D v = object.getTransform().getVelocity();
        PolygonGroupBounds bounds = object.getBounds();
        float x = object.getX();
        float y = object.getY();
        float z = object.getZ();
        float r = bounds.getRadius();
        float stepSize = 0;
        if (!object.isFlying()) {
            stepSize = BSPPolygon.PASSABLE_WALL_THRESHOLD;
        }
        float bottom = object.getY() + bounds.getBottomHeight() +
            stepSize;
        float top = object.getY() + bounds.getTopHeight();

        // wybierz najbliższy punkt przecięcia dla 4 wierzchołków
        BSPPolygon closestWall = null;
        float closestDistSq = Float.MAX_VALUE;
        for (int i=0; i<CORNERS.length; i++) {
            float xOffset = r * CORNERS[i].x;
            float zOffset = r * CORNERS[i].y;
            BSPPolygon wall = getFirstWallIntersection(
                oldLocation.x+xOffset, oldLocation.z+zOffset,
                x+xOffset, z+zOffset, bottom, top);
            if (wall != null) {
                float x2 = intersection.x-xOffset;
                float z2 = intersection.y-zOffset;
                float dx = (x2-oldLocation.x);
                float dz = (z2-oldLocation.z);
                float distSq = dx*dx + dz*dz;
                // wybierz najbliższą ścianę lub - jeśli odległości
                // do ścian są równe - wybierz bieżącą ścianę (jeśli
                // jej przód jest zgodny z zwrotem wektora ruchu).

                if (distSq < closestDistSq ||
                    (distSq == closestDistSq &&
                    MoreMath.sign(xOffset) == MoreMath.sign(v.x) &&
                    MoreMath.sign(zOffset) == MoreMath.sign(v.z)))
                {
                    closestWall = wall;
                    closestDistSq = distSq;
                    object.getLocation().setTo(x2, y, z2);
                }
            }
        }

        if (closestWall != null) {
            object.notifyWallCollision();
        }

        // upewnij się, że bryła otaczająca gracza jest pusta
        // (nie koliduje z ostrymi narożnikami)
        x = object.getX();
        z = object.getZ();
        r-=1;
        for (int i=0; i<CORNERS.length; i++) {
            int next = i+1;
            if (next == CORNERS.length) {
                next = 0;
            }
            // użyj wartości (r-1), by nie przeszkadzać w obliczeniach
            // dla zwykłych kolizji
            float xOffset1 = r * CORNERS[i].x;
            float zOffset1 = r * CORNERS[i].y;
            float xOffset2 = r * CORNERS[next].x;
            float zOffset2 = r * CORNERS[next].y;

            BSPPolygon wall = getFirstWallIntersection(
                x+xOffset1, z+zOffset1, x+xOffset2, z+zOffset2,
                bottom, top);
            if (wall != null) {
                object.notifyWallCollision();
                object.getLocation().setTo(
                    oldLocation.x, object.getY(), oldLocation.z);
                return wall;
            }
        }

        return closestWall;
    }


    /**
        Zwraca punkt przecięcia (jeśli w ogóle istnieje) pomiędzy ścieżką
        (x1,z1)->(x2,z2) a ścianami reprezentowanymi w drzewie BSP.
        Zwraca albo pierwszy przecinany obiekt BSPPolygon, albo null (jeśli
        nie istnieje żaden punkt przecięcia).
    */
    public BSPPolygon getFirstWallIntersection(float x1, float z1,
        float x2, float z2, float yBottom, float yTop)
    {
        return getFirstWallIntersection(bspTree.getRoot(),
            x1, z1, x2, z2, yBottom, yTop);
    }


    /**
        Zwraca punkt przecięcia (jeśli w ogóle istnieje) pomiędzy ścieżką
        (x1,z1)->(x2,z2) a ścianami reprezentowanymi w drzewie BSP, począwszy
        od danego węzła. Zwraca albo pierwszy przecinany obiekt BSPPolygon,
        albo null (jeśli nie istnieje żaden punkt przecięcia).
    */
    protected BSPPolygon getFirstWallIntersection(
        BSPTree.Node node, float x1, float z1, float x2, float z2,
        float yBottom, float yTop)
    {
        if (node == null || node instanceof BSPTree.Leaf) {
            return null;
        }

        int start = node.partition.getSideThick(x1, z1);
        int end = node.partition.getSideThick(x2, z2);
        float intersectionX;
        float intersectionZ;

        if (end == BSPLine.COLLINEAR) {
            end = start;
        }

        if (start == BSPLine.COLLINEAR) {
            intersectionX = x1;
            intersectionZ = z1;
        }
        else if (start != end) {
            path.setLine(x1, z1, x2, z2);
            node.partition.getIntersectionPoint(path,intersection);
            intersectionX = intersection.x;
            intersectionZ = intersection.y;
        }
        else  {
            intersectionX = x2;
            intersectionZ = z2;
        }

        if (start == BSPLine.COLLINEAR && start == end) {
            return null;
        }

        // sprawdza przednią część linii podziału
        if (start != BSPLine.COLLINEAR) {
            BSPPolygon wall = getFirstWallIntersection(
                (start == BSPLine.FRONT)?node.front:node.back,
                x1, z1, intersectionX, intersectionZ,
                yBottom, yTop);
            if (wall != null) {
                return wall;
            }
        }

        // testuje otoczenie obiektu
        if (start != end || start == BSPLine.COLLINEAR) {
            BSPPolygon wall = getWallCollision(node.polygons,
                    x1, z1, x2, z2, yBottom, yTop);
            if (wall != null) {
                intersection.setLocation(intersectionX,
                    intersectionZ);
                return wall;
            }
        }

        // sprawdza tylną część linii podziału
        if (start != end) {
            BSPPolygon wall = getFirstWallIntersection(
                (end == BSPLine.FRONT)?node.front:node.back,
                intersectionX, intersectionZ, x2, z2,
                yBottom, yTop);
            if (wall != null) {
                return wall;
            }
        }

        // nie znaleziono punktu przecięcia
        return null;
    }


    /**
        Sprawdza, czy określona ścieżka koliduje z którymkolwiek z listy 
        wielokątów leżących na linii. Ścieżka przecina linię reprezentowaną
        przez wielokąty, jednak nie zawsze oznacza to, że przecina same
        wielokąty.
    */
    protected BSPPolygon getWallCollision(List polygons,
        float x1, float z1, float x2, float z2,
        float yBottom, float yTop)
    {
        path.setLine(x1, z1, x2, z2);
        for (int i=0; i<polygons.size(); i++) {
            BSPPolygon poly = (BSPPolygon)polygons.get(i);
            BSPLine wall = poly.getLine();

            // sprawdza, czy nie jest to ściana
            if (wall == null) {
                continue;
            }

            // sprawdza, czy znajduje się na tej samej wysokości (współrzędna y) co ściana
            if (wall.top <= yBottom || wall.bottom > yTop) {
                continue;
            }

            // sprawdza, czy ruch nie odbywa się od tyłu ściany
            if (wall.getSideThin(x2, z2) != BSPLine.BACK) {
                continue;
            }

            // sprawdza, czy ścieżka przecina samą ścianę
            int side1 = path.getSideThin(wall.x1, wall.y1);
            int side2 = path.getSideThin(wall.x2, wall.y2);
            if (side1 != side2) {
                return poly;
            }
        }
        return null;
    }


    /**
        Sprawdza, czy dany obiekt koliduje z dowolnym innym obiektem
        przechowywanym na przekazanej liście.
    */
    public boolean checkObject(GameObject objectA, List objects,
        Vector3D oldLocation)
    {
        boolean collision = false;
        for (int i=0; i<objects.size(); i++) {
            GameObject objectB = (GameObject)objects.get(i);
            collision |= checkObject(objectA, objectB,
                oldLocation);
        }
        return collision;
    }


    /**
        Zwraca wartość true, jeśli dwa przekazane obiekty ze sobą kolidują.
        Obiekt A porusza się, obiekt B podlega sprawdzeniu. Metoda wykorzystuje
        do wykrywania kolizji pionowe walce otaczające (o okrągłej podstawie).
    */
    public boolean checkObject(GameObject objectA,
        GameObject objectB, Vector3D oldLocation)
    {
        // nie może kolidować z samym sobą
        if (objectA == objectB) {
            return false;
        }

        PolygonGroupBounds boundsA = objectA.getBounds();
        PolygonGroupBounds boundsB = objectB.getBounds();

        // najpierw sprawdzamy kolizje w osi y (zakładamy, że wysokość jest stała)
        float Ay1 = objectA.getY() + boundsA.getBottomHeight();
        float Ay2 = objectA.getY() + boundsA.getTopHeight();
        float By1 = objectB.getY() + boundsB.getBottomHeight();
        float By2 = objectB.getY() + boundsB.getTopHeight();
        if (By2 < Ay1 || By1 > Ay2) {
            return false;
        }

        // następnie sprawdzamy dwa wymiary, kolizje powierzchni x/z 
        // (okrągłych podstaw walców)
        float dx = objectA.getX() - objectB.getX();
        float dz = objectA.getZ() - objectB.getZ();
        float minDist = boundsA.getRadius() + boundsB.getRadius();
        float distSq = dx*dx + dz*dz;
        float minDistSq = minDist * minDist;
        if (distSq < minDistSq) {
             return handleObjectCollision(objectA, objectB, distSq,
                minDistSq, oldLocation);
        }
        return false;
    }


    /**
        Obsługuje kolizję obiektu. Obiekt A porusza się, obiekt B jest tym
        obiektem, z którym koliduje obiekt A.
    */
    protected boolean handleObjectCollision(GameObject objectA,
        GameObject objectB, float distSq, float minDistSq,
        Vector3D oldLocation)
    {
        objectA.notifyObjectCollision(objectB);
        return true;
    }

}
