package com.brackeen.javagamebook.bsp2D;

import java.util.*;
import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.graphics3D.texture.*;

/**
    Klasa RoomDef reprezentuje wypukłe pomieszczenie ze ścianami,
    podłogą i sufitem. Podłoga może się znajdować ponad sufitem,
    w takie sytuacji obiekt RoomDef jest "filarem" lub "blokiem",
    nie "pomieszczeniem". Obiekty klasy RoomDef są wykorzystywane
    jako skróty do tworzenia obiektów klasy BSPPolygon używanych w
    dwuwymiarowym drzewie BSP.
*/
public class RoomDef {

    private static final Vector3D FLOOR_NORMAL =
        new Vector3D(0,1,0);

    private static final Vector3D CEIL_NORMAL =
        new Vector3D(0,-1,0);

    private HorizontalAreaDef floor;
    private HorizontalAreaDef ceil;
    private List vertices;
    private float ambientLightIntensity;


    /**
        Klasa HorizontalAreaDef reprezentuje podłogę lub sufit.
    */
    private static class HorizontalAreaDef {
        float height;
        Texture texture;
        Rectangle3D textureBounds;

        public HorizontalAreaDef(float height, Texture texture,
            Rectangle3D textureBounds)
        {
            this.height = height;
            this.texture = texture;
            this.textureBounds = textureBounds;
        }
    }


    /**
        Klasa Vertex reprezentuje wierzchołek ściany (obiektu Wall).
    */
    private static class Vertex {
        float x;
        float z;
        float bottom;
        float top;
        Texture texture;
        Rectangle3D textureBounds;

        public Vertex(float x, float z, float bottom, float top,
            Texture texture, Rectangle3D textureBounds)
        {
            this.x = x;
            this.z = z;
            this.bottom = bottom;
            this.top = top;
            this.texture = texture;
            this.textureBounds = textureBounds;
        }

        public boolean isWall() {
            return (bottom != top) && (texture != null);
        }
    }


    /**
        Tworzy nowy obiekt klasy RoomDef ze światłem otaczającym o intensywości
        0,5. Ta intensywność światła jest wykorzystywana dla wszystkich ścian,
        podłóg i sufitów.
    */
    public RoomDef() {
        this(0.5f);
    }


    /**
        Tworzy nowy obiekt klasy RoomDef ze światłem otaczającym o podanej
        intensywości. Ta intensywność światła jest wykorzystywana dla wszystkich
        ścian, podłóg i sufitów.
    */
    public RoomDef(float ambientLightIntensity) {
        this.ambientLightIntensity = ambientLightIntensity;
        vertices = new ArrayList();
    }


    /**
        Dodaje nowy wierzchołek ściany we wskazanym punkcie (x, z),
        ze wskazaną teksturą. Ściana łączy podłogę z sufitem. Jeśli
        tekstura jest równa null, dla ściany nie jest tworzony żaden
        wielokąt.
    */
    public void addVertex(float x, float z, Texture texture) {
        addVertex(x, z, Math.min(floor.height, ceil.height),
             Math.max(floor.height, ceil.height),
             texture);
    }


    /**
        Dodaje nowy wierzchołek ściany we wskazanym punkcie (x, z),
        ze wskazaną teksturą, wysokością podłogi i sufitu. Jeśli
        tekstura jest równa null, dla ściany nie jest tworzony żaden
        wielokąt.
    */
    public void addVertex(float x, float z,
        float bottom, float top, Texture texture)
    {
        vertices.add(
            new Vertex(x, z, bottom, top, texture, null));
    }


    /**
        Dodaje nowy wierzchołek ściany we wskazanym punkcie (x, z),
        ze wskazaną teksturą, ograniczeniami tekstury oraz wysokością
        podłogi i sufitu. Jeśli tekstura jest równa null, dla ściany
        nie jest tworzony żaden wielokąt.
    */
    public void addVertex(float x, float z, float bottom,
        float top, Texture texture, Rectangle3D texBounds)
    {
        vertices.add(
            new Vertex(x, z, bottom, top, texture, texBounds));
    }


    /**
        Ustawia wysokość i teksturę dla podłogi w tym pomieszczeniu.
        Jeśli tekstura jest równa null, dla podłogi nie jest tworzony
        żaden wielokąt. jednak wysokość podłogi jest wykorzystywana
        jako domyślne dolne ograniczenie ścian.
    */
    public void setFloor(float height, Texture texture) {
        setFloor(height, texture, null);
    }


    /**
        Ustawia wysokość, teksturę i ograniczenia tekstury dla podłogi
        w tym pomieszczeniu. Jeśli tekstura jest równa null, dla podłogi
        nie jest tworzony żaden wielokąt. jednak wysokość podłogi jest
        wykorzystywana jako domyślne dolne ograniczenie ścian. Jeśli
        ograniczenia tekstury są równe null, wykorzystywane są domyślne
        ograniczenia.
    */
    public void setFloor(float height, Texture texture,
        Rectangle3D texBounds)
    {
        if (texture != null && texBounds == null) {
            texBounds = new Rectangle3D(
                new Vector3D(0,height,0),
                new Vector3D(1,0,0),
                new Vector3D(0,0,-1),
                texture.getWidth(), texture.getHeight());
        }
        floor = new HorizontalAreaDef(height, texture, texBounds);
    }


    /**
        Ustawia wysokość i teksturę dla sufitu w tym pomieszczeniu.
        Jeśli tekstura jest równa null, dla sufitu nie jest tworzony
        żaden wielokąt. jednak wysokość sufitu jest wykorzystywana
        jako domyślne górne ograniczenie ścian.
    */
    public void setCeil(float height, Texture texture) {
        setCeil(height, texture, null);
    }


     /**
        Ustawia wysokość, teksturę i ograniczenia tekstury dla sufitu
        w tym pomieszczeniu. Jeśli tekstura jest równa null, dla sufitu
        nie jest tworzony żaden wielokąt. jednak wysokość sufitu jest
        wykorzystywana jako domyślne górne ograniczenie ścian. Jeśli
        ograniczenia tekstury są równe null, wykorzystywane są domyślne
        ograniczenia.
    */
    public void setCeil(float height, Texture texture,
        Rectangle3D texBounds)
    {
        if (texture != null && texBounds == null) {
            texBounds = new Rectangle3D(
                new Vector3D(0,height,0),
                new Vector3D(1,0,0),
                new Vector3D(0,0,1),
                texture.getWidth(), texture.getHeight());
        }
        ceil = new HorizontalAreaDef(height, texture, texBounds);
    }


    /**
        Tworzy i zwraca listę obiektów klasy BSPPolygon reprezentujących
        ściany, podłogę i sufit w tym pomieszczeniu.
    */
    public List createPolygons() {
        List walls = createVerticalPolygons();
        List floors = createHorizontalPolygons();

        List list = new ArrayList(walls.size() + floors.size());
        list.addAll(walls);
        list.addAll(floors);
        return list;
    }


    /**
        Tworzy i zwraca listę obiektów klasy BSPPolygon reprezentujących
        pionowe ściany w tym pomieszczeniu.
    */
    public List createVerticalPolygons() {
        int size = vertices.size();
        List list = new ArrayList(size);
        if (size == 0) {
            return list;
        }
        Vertex origin = (Vertex)vertices.get(0);
        Vector3D textureOrigin =
            new Vector3D(origin.x, ceil.height, origin.z);
        Vector3D textureDy = new Vector3D(0,-1,0);

        for (int i=0; i<size; i++) {
            Vertex curr = (Vertex)vertices.get(i);

            if (!curr.isWall()) {
                continue;
            }

            // określ, czy przez ścianę można przejść (przydatne dla bram)
            int type = BSPPolygon.TYPE_WALL;
            if (floor.height > ceil.height) {
                if (floor.height - ceil.height <=
                    BSPPolygon.PASSABLE_WALL_THRESHOLD)
                {
                    type = BSPPolygon.TYPE_PASSABLE_WALL;
                }
            }
            else if (curr.top - curr.bottom <=
               BSPPolygon.PASSABLE_WALL_THRESHOLD)
            {
                type = BSPPolygon.TYPE_PASSABLE_WALL;
            }
            else if (curr.bottom - floor.height >=
                BSPPolygon.PASSABLE_ENTRYWAY_THRESHOLD)
            {
                type = BSPPolygon.TYPE_PASSABLE_WALL;
            }

            List wallVertices = new ArrayList();
            Vertex prev;
            Vertex next;
            if (floor.height < ceil.height) {
                prev = (Vertex)vertices.get((i+size-1) % size);
                next = (Vertex)vertices.get((i+1) % size);
            }
            else {
                prev = (Vertex)vertices.get((i+1) % size);
                next = (Vertex)vertices.get((i+size-1) % size);
            }

            // dolne wierzchołki
            wallVertices.add(
                new Vector3D(next.x, curr.bottom, next.z));
            wallVertices.add(
                new Vector3D(curr.x, curr.bottom, curr.z));

            // opcjonalne wierzchołki T-złączy po lewej stronie
            if (prev.isWall()) {
                if (prev.bottom > curr.bottom &&
                    prev.bottom < curr.top)
                {
                    wallVertices.add(
                        new Vector3D(curr.x, prev.bottom, curr.z));
                }
                if (prev.top > curr.bottom &&
                    prev.top < curr.top)
                {
                    wallVertices.add(
                        new Vector3D(curr.x, prev.top, curr.z));
                }

            }

            // górne wierzchołki
            wallVertices.add(
                new Vector3D(curr.x, curr.top, curr.z));
            wallVertices.add(
                new Vector3D(next.x, curr.top, next.z));

            // opcjonalne wierzchołki T-złączy po lewej stronie
            if (next.isWall()) {
                if (next.top > curr.bottom &&
                    next.top < curr.top)
                {
                    wallVertices.add(
                        new Vector3D(next.x, next.top, next.z));
                }
                if (next.bottom > curr.bottom &&
                    next.bottom < curr.top)
                {
                    wallVertices.add(
                        new Vector3D(next.x, next.bottom, next.z));
                }

            }

            // stwórz wielokąt ściany
            Vector3D[] array = new Vector3D[wallVertices.size()];
            wallVertices.toArray(array);
            BSPPolygon poly = new BSPPolygon(array, type);
            poly.setAmbientLightIntensity(ambientLightIntensity);
            if (curr.textureBounds == null) {
                Vector3D textureDx = new Vector3D(next.x,0,next.z);
                textureDx.subtract(new Vector3D(curr.x,0,curr.z));
                textureDx.normalize();
                curr.textureBounds = new Rectangle3D(
                    textureOrigin,
                    textureDx,
                    textureDy,
                    curr.texture.getWidth(),
                    curr.texture.getHeight());
            }
            poly.setTexture(curr.texture, curr.textureBounds);
            list.add(poly);
        }
        return list;
    }


    /**
        Tworzy i zwraca listę obiektów klasy BSPPolygon reprezentujących
        poziome podłogi i sufity w tym pomieszczeniu.
    */
    public List createHorizontalPolygons() {

        List list = new ArrayList(2);
        int size = vertices.size();
        Vector3D[] floorVertices = new Vector3D[size];
        Vector3D[] ceilVertices = new Vector3D[size];

        // stwórz wierzchołki
        for (int i=0; i<size; i++) {
            Vertex v = (Vertex)vertices.get(i);
            floorVertices[i] =
                new Vector3D(v.x, floor.height, v.z);
            ceilVertices[size-(i+1)] =
                new Vector3D(v.x, ceil.height, v.z);
        }

        // stwórz wielokąt podłogi
        if (floor.texture != null) {
            BSPPolygon poly = new BSPPolygon(floorVertices,
                BSPPolygon.TYPE_FLOOR);
            poly.setTexture(floor.texture, floor.textureBounds);
            poly.setNormal(FLOOR_NORMAL);
            poly.setAmbientLightIntensity(ambientLightIntensity);
            list.add(poly);
        }

        // stwórz wielokąt sufitu
        if (ceil.texture != null) {
            BSPPolygon poly = new BSPPolygon(ceilVertices,
                BSPPolygon.TYPE_FLOOR);
            poly.setTexture(ceil.texture, ceil.textureBounds);
            poly.setNormal(CEIL_NORMAL);
            poly.setAmbientLightIntensity(ambientLightIntensity);
            list.add(poly);
        }

        return list;
    }

}
