package com.brackeen.javagamebook.math3D;

/**
    Klasa Polygon3D przedstawia wielokąt jako zbiór
    wierzchołków.
*/
public class Polygon3D implements Transformable {

    // tymczasowe wektory wykorzystywane do obliczeń
    private static Vector3D temp1 = new Vector3D();
    private static Vector3D temp2 = new Vector3D();

    private Vector3D[] v;
    private int numVertices;
    private Vector3D normal;

    /**
        Tworzy pusty wielokąt, który można wykorzystywać jako
        wielokąt „roboczy” dla potrzeb transformacji, rzutowania, itd. 
    */
    public Polygon3D() {
        numVertices = 0;
        v = new Vector3D[0];
        normal = new Vector3D();
    }


    /**
       Tworzy nowy wielokąt Polygon3D o podanych wierzchołkach.
    */
    public Polygon3D(Vector3D v0, Vector3D v1, Vector3D v2) {
        this(new Vector3D[] { v0, v1, v2 });
    }


    /**
        Tworzy nowy wielokąt Polygon3D o podanych wierzchołkach. Zakładamy,
        że wszystkie wierzchołki położone są w tej samej płaszczyźnie.
    */
    public Polygon3D(Vector3D v0, Vector3D v1, Vector3D v2,
        Vector3D v3)
    {
        this(new Vector3D[] { v0, v1, v2, v3 });
    }


    /**
        Tworzy nowy wielokąt Polygon3D o podanych wierzchołkach. Zakładamy,
        że wszystkie wierzchołki położone są w tej samej płaszczyźnie.
    */
    public Polygon3D(Vector3D[] vertices) {
        this.v = vertices;
        numVertices = vertices.length;
        calcNormal();
    }


    /**
        Przypisuje temu wielokątowi takie same wierzchołki jakie ma 
        podany wielokąt.
    */
    public void setTo(Polygon3D polygon) {
        numVertices = polygon.numVertices;
        normal.setTo(polygon.normal);

        ensureCapacity(numVertices);
        for (int i=0; i<numVertices; i++) {
            v[i].setTo(polygon.v[i]);
        }
    }


    /**
        Upewnia się, że wielokąt ma wystarczającą pojemność,
        żeby przechowywać dane o wszystkich swoich wierzchołkach.
    */
    protected void ensureCapacity(int length) {
        if (v.length < length) {
            Vector3D[] newV = new Vector3D[length];
            System.arraycopy(v,0,newV,0,v.length);
            for (int i=v.length; i<newV.length; i++) {
                newV[i] = new Vector3D();
            }
            v = newV;
        }
    }


    /**
        Pobiera liczbę wierzchołków, które ma ten wielokąt.
    */
    public int getNumVertices() {
        return numVertices;
    }


    /**
        Pobiera wierzchołek o podanym indeksie.
    */
    public Vector3D getVertex(int index) {
        return v[index];
    }


    /**
        Rzutuje ten wielokąt na okno obrazu.
    */
    public void project(ViewWindow view) {
        for (int i=0; i<numVertices; i++) {
            view.project(v[i]);
        }
    }


    // methods from the Transformable interface.

    public void add(Vector3D u) {
       for (int i=0; i<numVertices; i++) {
           v[i].add(u);
       }
    }

    public void subtract(Vector3D u) {
       for (int i=0; i<numVertices; i++) {
           v[i].subtract(u);
       }
    }

    public void add(Transform3D xform) {
        addRotation(xform);
        add(xform.getLocation());
    }

    public void subtract(Transform3D xform) {
        subtract(xform.getLocation());
        subtractRotation(xform);
    }

    public void addRotation(Transform3D xform) {
        for (int i=0; i<numVertices; i++) {
           v[i].addRotation(xform);
        }
        normal.addRotation(xform);
    }

    public void subtractRotation(Transform3D xform) {
        for (int i=0; i<numVertices; i++) {
           v[i].subtractRotation(xform);
        }
        normal.subtractRotation(xform);
    }

    /**
        Wylicza jednostkowy wektor normalny dla tego wielokąta.
        Metoda ta wykorzystuje pierwszy, drugi i trzeci wierzchołek, 
        by wyliczyć wektor normalny, więc jeśli wierzchołki te leżą w 
        jednej linii, to metoda ta nie będzie działać. W tym przypadku,
        można otrzymać wektor normalny z boków wielokąta.
        Metoda setNormal() pozwala ręcznie ustawić wektor normalny.
        Wykorzystuje ona do obliczeń statyczne obiekty w klasie Polygon3D, 
        więc nie gwarantuje bezpieczeństwa obliczeń w przypadku wielu
        równolegle wykorzystywanych instancji klasy Polygon3D.
    */
    public Vector3D calcNormal() {
        if (normal == null) {
            normal = new Vector3D();
        }
        temp1.setTo(v[2]);
        temp1.subtract(v[1]);
        temp2.setTo(v[0]);
        temp2.subtract(v[1]);
        normal.setToCrossProduct(temp1, temp2);
        normal.normalize();
        return normal;
    }


    /**
        Pobiera wektor normalny tego wielokąta. Jeśli wierzchołki się
        zmienią, należy użyć calcNormal().
    */
    public Vector3D getNormal() {
        return normal;
    }


    /**
        Definiuje wektor normalny dla tego wielokąta.
    */
    public void setNormal(Vector3D n) {
        if (normal == null) {
            normal = new Vector3D(n);
        }
        else {
            normal.setTo(n);
        }
    }

    /**
        Sprawdza, czy wielokąt zwrócony jest przodem do określonego punktu.
        Metoda ta wykorzystuje do obliczeń statyczne obiekty w klasie Polygon3D, 
        więc nie gwarantuje bezpieczeństwa obliczeń w przypadku wielu
        równolegle wykorzystywanych instancji klasy Polygon3D.
    */
    public boolean isFacing(Vector3D u) {
        temp1.setTo(u);
        temp1.subtract(v[0]);
        return (normal.getDotProduct(temp1) >= 0);
    }

    /**
        Przycina ten wielokąt w taki sposób, aby wszystkie jego
        wierzchołki znalazły się przed płaszczyzną przycinania, clipZ 
        (czyli, aby ich współrzędne z spełniały warunek z <= cilpZ).
        Wartość clipZ powinna być różna od 0, aby uniknąć problemów
        związanych z dzieleniem przez 0.
        Zwraca true (prawda), jeśli wielokąt przynajmniej częściowo
        znajduje się przed powierzchnią przycinania.
    */
    public boolean clip(float clipZ) {
        ensureCapacity(numVertices * 3);

        boolean isCompletelyHidden = true;

        // wstawia nowe wierzchołki, dzięki którym każda z krawędzi będzie,
        // albo całkowicie przed, albo całkowicie za płaszczyzną przycinania
        for (int i=0; i<numVertices; i++) {
            int next = (i + 1) % numVertices;
            Vector3D v1 = v[i];
            Vector3D v2 = v[next];
            if (v1.z < clipZ) {
                isCompletelyHidden = false;
            }
            // sprawdź czy v1.z < v2.z
            if (v1.z > v2.z) {
                Vector3D temp = v1;
                v1 = v2;
                v2 = temp;
            }
            if (v1.z < clipZ && v2.z > clipZ) {
                float scale = (clipZ-v1.z) / (v2.z - v1.z);
                insertVertex(next,
                    v1.x + scale * (v2.x - v1.x) ,
                    v1.y + scale * (v2.y - v1.y),
                    clipZ);
                // pomiń wierzchołek, który właśnie utworzyliśmy
                i++;
            }
        }

        if (isCompletelyHidden) {
            return false;
        }

        // usuń wszystkie wierzchołki, dla których z > clipZ
        for (int i=numVertices-1; i>=0; i--) {
            if (v[i].z > clipZ) {
                deleteVertex(i);
            }
        }

        return (numVertices >= 3);
    }

    /**
        Wstawia nowy wierzchołek pod podanym indeksem.
    */
    protected void insertVertex(int index, float x, float y,
        float z)
    {
        Vector3D newVertex = v[v.length-1];
        newVertex.x = x;
        newVertex.y = y;
        newVertex.z = z;
        for (int i=v.length-1; i>index; i--) {
            v[i] = v[i-1];
        }
        v[index] = newVertex;
        numVertices++;
    }


    /**
        Usuwa wierzchołek o podanym indeksie.
    */
    protected void deleteVertex(int index) {
        Vector3D deleted = v[index];
        for (int i=index; i<v.length-1; i++) {
            v[i] = v[i+1];
        }
        v[v.length-1] = deleted;
        numVertices--;
    }

    /**
        Wstawia wierzchołek do wielokąta pod podanym indeksem.
        Wstawiany jest dokładnie ten wierzchołek (a nie jego kopia).
    */
    public void insertVertex(int index, Vector3D vertex) {
        Vector3D[] newV = new Vector3D[numVertices+1];
        System.arraycopy(v,0,newV,0,index);
        newV[index] = vertex;
        System.arraycopy(v,index,newV,index+1,numVertices-index);
        v = newV;
        numVertices++;
    }

}
