package com.brackeen.javagamebook.bsp2D;

import java.awt.geom.*;
import com.brackeen.javagamebook.math3D.*;

public class BSPLine extends Line2D.Float {

    public static final int BACK = -1;
    public static final int COLLINEAR = 0;
    public static final int FRONT = 1;
    public static final int SPANNING = 2;

    /**
        Współrzędna X wektora prostopadłego.
    */
    public float nx;

    /**
        Współrzędna Y wektora prostopadłego.
    */
    public float ny;

    /**
        Najwyższe położenie linii reprezentującej ścianę.
    */
    public float top;

    /**
        Najniższe położenie linii reprezentującej ścianę.
    */
    public float bottom;

    /**
        Tworzy nową linię z punktu (0,0) do punktu (0,0)
    */
    public BSPLine() {
        super();
    }


    /**
        Tworzy nowy obiekt BSPLine dla przekazanego obiektu BSPPolygon
        (tylko jeśli obiekt BSPPolygon reprezentuje pionową ścianę).
    */
    public BSPLine(BSPPolygon poly) {
        setTo(poly);
    }


    /**
        Tworzy nowy obiekt BSPLine dla przekazanych współrzędnych.
    */
    public BSPLine(float x1, float y1, float x2, float y2) {
        setLine(x1, y1, x2, y2);
    }


    /**
        Łączy ten obiekt BSPLine z danym obiektem BSPPolygon
        (tylko jeśli obiekt BSPPolygon reprezentuje pionową ścianę).
    */
    public void setTo(BSPPolygon poly) {
        if (!poly.isWall()) {
            throw new IllegalArgumentException(
                "BSPPolygon not a wall");
        }
        top = java.lang.Float.MIN_VALUE;
        bottom = java.lang.Float.MAX_VALUE;
        // znajduje parę najbardziej oddalonych punktów (ignorując współrzędną y)
        float distance = -1;
        for (int i=0; i<poly.getNumVertices(); i++) {
            Vector3D v1 = poly.getVertex(i);
            top = Math.max(top, v1.y);
            bottom = Math.min(bottom, v1.y);
            for (int j=0; j<poly.getNumVertices(); j++) {
                Vector3D v2 = poly.getVertex(j);
                float newDist = (float)Point2D.distanceSq(
                    v1.x, v1.z, v2.x, v2.z);
                if (newDist > distance) {
                    distance = newDist;
                    x1 = v1.x;
                    y1 = v1.z;
                    x2 = v2.x;
                    y2 = v2.z;
                }
            }
        }
        nx = poly.getNormal().x;
        ny = poly.getNormal().z;
    }

    /**
        Oblicza dane prostej prostopadłej do tej linii.
    */
    public void calcNormal() {
        nx = y2 - y1;
        ny = x1 - x2;
    }

    /**
        Normalizuje długość wektora prostopadłego do linii podziału (zmienia
        jego długość na 1 jednostkę).
    */
    public void normalize() {
        float length = (float)Math.sqrt(nx * nx + ny * ny);
        nx/=length;
        ny/=length;
    }


    public void setLine(float x1, float y1, float x2, float y2) {
        super.setLine(x1, y1, x2, y2);
        calcNormal();
    }


    public void setLine(double x1, double y1, double x2,
        double y2)
    {
        super.setLine(x1, y1, x2, y2);
        calcNormal();
    }


    /**
        Zamienia punkty końcowe tej linii (innymi słowy, (x1,y1) staje się (x2,y2)
        i odwrotnie) oraz odwraca kierunek wskazywany przez prostpadłą.
    */
    public void flip() {
        float tx = x1;
        float ty = y1;
        x1 = x2;
        y1 = y2;
        x2 = tx;
        y2 = ty;
        nx = -nx;
        ny = -ny;
    }


    /**
        Ustawia wysokość na jakiej znajduje się dolna i górna krawędź "ściany".
    */
    public void setHeight(float top, float bottom) {
        this.top = top;
        this.bottom = bottom;
    }


    /**
        Zwraca true, jeśli punkty końcowe tej linii pokrywają się z punktami
        końcowymi przekazanej linii. Ignoruje prostopadłą i wartości dotyczące
        wysokości.
    */
    public boolean equals(BSPLine line) {
        return (x1 == line.x1 && x2 == line.x2 &&
            y1 == line.y1 && y2 == line.y2);
    }


    /**
        Zwraca true, jeśli punkty końcowe tej linii pokrywają się z punktami
        końcowymi przekazanej linii. Ignoruje kolejność punktów końcowych
        (jeśli pierwszy punkt końcowy tej linii pokrywa się z drugim punktem
        końcowym przekazanej linii, i odwrotnie, metoda zwraca true). Ignoruje
        także prostopadłą i wartości dotyczące wysokości.
    */
    public boolean equalsIgnoreOrder(BSPLine line) {
        return equals(line) || (
            (x1 == line.x2 && x2 == line.x1 &&
            y1 == line.y2 && y2 == line.y1));
    }


    public String toString() {
        return "(" + x1 + ", " + y1 + ")->" +
            "(" + x2 + "," + y2 + ")" +
            " bottom: " + bottom + " top: " + top;
    }


    /**
        Zwraca stronę, po której znajduje się określony punkt względem linii
        podziału. Metoda zakłada, że linia podziału ma grubość jednej jednostki,
        zatem punkty znajdujące się w granicach tej pogrubionej linii są traktowane
        jak punkty należące do linii podziału. Aby ta metoda działała prawidłowo, 
        należy znormalizować długość wektora prostopadłego do linii podziału (albo 
        wiążąc tę linię z wielokątem, albo wywołując metodę normalize()).
        W zależności od położenia punktu, metoda zwraca wartość FRONT, BACK lub 
        COLLINEAR.
    */
    public int getSideThick(float x, float y) {
        int frontSide = getSideThin(x-nx/2, y-ny/2);
        if (frontSide == FRONT) {
            return FRONT;
        }
        else if (frontSide == BACK) {
            int backSide = getSideThin(x+nx/2, y+ny/2);
            if (backSide == BACK) {
                return BACK;
            }
        }
        return COLLINEAR;
    }


    /**
        Zwraca stronę, po której znajduje się określony punkt względem linii
        podziału. Ze względu na niedokładność obliczeń na liczbach zmiennoprze-
        cinkowych, przypadki wykrycia przynależności punktu do linii podziału
        są bardzo rzadkie. Aby ta metoda działała prawidłowo, należy znormalizować
        długość wektora prostopadłego do linii podziału (albo wiążąc tę linię z
        wielokątem, albo wywołując metodę normalize()).
        W zależności od położenia punktu, metoda zwraca wartość FRONT, BACK lub 
        COLLINEAR.
    */
    public int getSideThin(float x, float y) {
        // il. skal. wektora prowadzącego do punktu i w. prostopadłego do l. podziału
        float side = (x - x1)*nx + (y - y1)*ny;
        return (side < 0)?BACK:(side > 0)?FRONT:COLLINEAR;
    }


    /**
        Zwraca stronę (względem linii podziału), po której znajduje się
        przekazany odcinek. Zwraca wartość FRONT, BACK, COLINEAR lub SPANNING.
    */
    public int getSide(Line2D.Float segment) {
        if (this == segment) {
            return COLLINEAR;
        }
        int p1Side = getSideThick(segment.x1, segment.y1);
        int p2Side = getSideThick(segment.x2, segment.y2);
        if (p1Side == p2Side) {
            return p1Side;
        }
        else if (p1Side == COLLINEAR) {
            return p2Side;
        }
        else if (p2Side == COLLINEAR) {
            return p1Side;
        }
        else {
            return SPANNING;
        }
    }


    /**
        Zwraca stronę (względem linii podziału), po której znajduje się
        przekazany wielokąt. Zwraca wartość FRONT, BACK, COLINEAR lub 
        SPANNING.
    */
    public int getSide(BSPPolygon poly) {
        boolean onFront = false;
        boolean onBack = false;

        // sprawdza każdy punkt
        for (int i=0; i<poly.getNumVertices(); i++) {
            Vector3D v = poly.getVertex(i);
            int side = getSideThick(v.x, v.z);
            if (side == BSPLine.FRONT) {
                onFront = true;
            }
            else if (side == BSPLine.BACK) {
                onBack = true;
            }
        }

        // klasyfikuje wielomian
        if (onFront && onBack) {
            return BSPLine.SPANNING;
        }
        else if (onFront) {
            return BSPLine.FRONT;
        }
        else if (onBack) {
            return BSPLine.BACK;
        }
        else {
            return BSPLine.COLLINEAR;
        }
    }


    /**
        Zwraca stosunek długości jednej z części przeciętego odcinka
        do długości całego odcinka. Jeśli odcinki się przecinają,
        zwraca wartość od 0 do 1. Przykładowo, zwrócona wartość 0
        oznacza, że przecięcie nastąpiło w punkcie (x1, y1); wartość
        1 oznacza, że przecięcie nastąpiło w punkcie (x2, y2);
        natomiast wartość 0,5 oznacza, że przecięcie nastąpiło w środku
        odcinka. Zwraca wartość -1, jeśli odcinki są równoległe.
    */
    public float getIntersection(Line2D.Float line) {
        // Punkt przecięcia I dwóch wektorów, A1->A2 i B1->B2:
        // I = A1 + u * (A2 - A1)
        // I = B1 + v * (B2 - B1)
        //
        // Rozwiązanie dla u daje nam następujący wzór.
        // Wynikiem równania jest wartość u.
        float denominator = (line.y2 - line.y1) * (x2 - x1) - (line.x2 - line.x1) * (y2 - y1);

        // Sprawdza, czy dwie badane proste są równoległe
        if (denominator == 0) {
            return -1;
        }

        float numerator = (line.x2 - line.x1) * (y1 - line.y1) - (line.y2 - line.y1) * (x1 - line.x1);

        return numerator / denominator;
    }


    /**
        Zwraca współrzędne punktu przecięcia danej prostej i prostej
        przekazanej w formie argumentu.
    */
    public Point2D.Float getIntersectionPoint(Line2D.Float line) {
        return getIntersectionPoint(line, null);
    }


    /**
        Zwraca współrzędne punktu przecięcia danej prostej i prostej
        przekazanej w formie argumentu. Jeśli argument intersection
        ma wartość null, tworzony jest nowy punkt.
    */
    public Point2D.Float getIntersectionPoint(Line2D.Float line,
        Point2D.Float intersection)
    {
        if (intersection == null) {
            intersection = new Point2D.Float();
        }
        float fraction = getIntersection(line);
        intersection.setLocation(
            x1 + fraction * (x2 - x1),
            y1 + fraction * (y2 - y1));
        return intersection;
    }

}
