package com.brackeen.javagamebook.bsp2D;

import java.io.*;
import java.util.*;
import com.brackeen.javagamebook.math3D.*;

/**
    Klasa MapLoader wczytuje mapy z pliku tekstowego zbudowanego
    zgodnie z ze specyfikacją plików OBJ Alias|Wavefront.

    Polecenia pliku MAP:
    <pre>
    v [x] [y] [z]        - Definiuje wierzchołek o współrzędnych
                           zmiennoprzecinkowych (x,y,z).
    mtllib [filename]    - Ładuje materiały z zewnętrznego pliku 
                           .mtl.
    usemtl [name]        - Wykorzystuje materiał o podanej nazwie (ładowany
                           z pliku .mtl) na następną podłogę, sufit lub
                           ścianę.
    ambientLightIntensity
        [value]          - Definiuje intensywność światła otoczenia 
                           dla kolejnego pokoju, w przedziale od 0 do 1.
    pointlight [v]       - Definiuje punktowe światło zlokalizowane pod 
        [intensity]        określonym wektorem. Opcjonalnie mozna podać
        [falloff]          intensywność światła i odległość całkowitego 
                           wygasania światła ze źródła.
    player [v] [angle]   - Definiuje początkową lokację gracza i  
                           opcjonalnie początkowy kąt, mierzony 
                           w radianach względem osi y.
    obj [uniqueName]     - Definiuje obiekt z zewnęyrznego pliku
        [filename] [v]     OBJ. Ta unikatowa nazwa pozwala jednoznacznie 
        [angle]            zidentyfikować obiekt, mozna jej jednak także  
                           przypisać wartość "null", jeśli nie potrzebujemy
                           unikatowej nazwy. Nazwa pliku odnosi się do
                           zewnętrznego pliku OBJ. Opcjonalnie mozna podać 
                           początkowy kąt, mierzony w radianach 
                           względem osi y.
    room [name]          - Definiuje nowy pokój, opcjonalnie nadając mu
                           nazwę. Pokój składa się z pionowych ścian,
                           poziomej podłogi i poziomego sufitu.
                           Wklęsłe pomieszczenia nie są na razie 
                           obsługiwane, można je jednak symulować
                           za pomocą sąsiadujących ze sobą wypukłych pokoi.
    floor [height]       - Definiuje wysokość podłogi w bieżącym 
                           pokoju, wykorzystując bieżący materiał.
                           Bieżącemu materiałowi mozna przypisać null
                           i wtedy nie jest tworzona żaden wielokąt
                           podłogi. Podłoga może też znajdować się nad 
                           sufitem i wtedy zamiast pokoju tworzona jest
                           struktura "filaru" lub "skalnego bloku",
                           niedostępna dla gracza.
    ceil [height]        - Definiuje wysokość sufitu w biezącym pokoju,
                           wykorzystując w tym celu biezacy materiał.
                           Bieżącemu materiałowi mozna przypisać null
                           i wtedy nie jest tworzona żaden wielokąt
                           sufitu. Sufit może też znajdować się pod 
                           podłogą i wtedy zamiast pokoju tworzona jest
                           struktura "filaru" lub "skalnego bloku",
                           niedostępna dla gracza.
    wall [x] [z]         - Dwfiniuje wierzchołek ściany w pokoju używając
         [bottom] [top]    podanych współrzędnych x i z.Ściany powinny 
                           być definiowane w porządku zgodnym z ruchem 
                           wskazówek zegara. Jeśli nie zostanie zdefinowany
                           "dół" i "góra", wykorzystywane są wysokości 
                           podłogii sufitu. Jeśli biezący materiał jest
                           null lub dół [bottom] jest równy górze [top],
                           to wielokąt ściany nie zostanie utworzony.
    </pre>
*/
public class MapLoader extends ObjectLoader {

    private BSPTreeBuilder builder;
    private Map loadedObjects;
    private Transform3D playerStart;
    private RoomDef currentRoom;
    private List rooms;
    private List mapObjects;

    // użyj osobnego obiektu klasy ObjectLoader dla obiektów
    private ObjectLoader objectLoader;


    /**
        Tworzy nowy obiekt klasy MapLoader wykorzystujący domyślny obiekt BSPTreeBuilder.
    */
    public MapLoader() {
        this(null);
    }


    /**
        Tworzy nowy obiekt klasy MapLoader wykorzystujący wskazany obiekt BSPTreeBuilder.
        Jeśli wskazany obiekt jest równy null, tworzony jest domyślny obiekt BSPTreeBuilder.
    */
    public MapLoader(BSPTreeBuilder builder) {
        if (builder == null) {
            this.builder = new BSPTreeBuilder();
        }
        else {
            this.builder = builder;
        }
        parsers.put("map", new MapLineParser());
        objectLoader = new ObjectLoader();
        loadedObjects = new HashMap();
        rooms = new ArrayList();
        mapObjects = new ArrayList();
    }


    /**
        Wczytuje plik mapy i tworzy drzewo BSP. Tworzone obiekty można odczytywać za
        pomocą metody getObjectsInMap().
    */
    public BSPTree loadMap(String filename) throws IOException {
        currentRoom = null;
        rooms.clear();
        vertices.clear();
        mapObjects.clear();
        playerStart = new Transform3D();

        path = new File(filename).getParentFile();

        parseFile(filename);

        return createBSPTree();
    }


    /**
        Tworzy drzewo BSP na podstawie pomieszczeń zdefiniowanych w pliku mapy.
    */
    protected BSPTree createBSPTree() {
        // odczytaj wszystkie wielokąty
        List allPolygons = new ArrayList();
        for (int i=0; i<rooms.size(); i++) {
            RoomDef room = (RoomDef)rooms.get(i);
            allPolygons.addAll(room.createPolygons());
        }

        // buduj drzewo
        BSPTree tree = builder.build(allPolygons);

        // stwórz powierzchnie wielokątów z uwzględnieniem świateł
        tree.createSurfaces(lights);
        return tree;
    }


    /**
        Zwraca listę wszystkich obiektów zdefiniowanych w pliku mapy.
    */
    public List getObjectsInMap() {
        return mapObjects;
    }


    /**
        Zwraca początkowe położenie gracza zdefiniowane w pliku mapy.
    */
    public Transform3D getPlayerStartLocation() {
        return playerStart;
    }


    /**
        Ustawia światła wykorzystywane dla obiektów OBJ.
    */
    public void setObjectLights(List lights,
        float ambientLightIntensity)
    {
        objectLoader.setLights(lights, ambientLightIntensity);
    }


    /**
        Parsuje pojedynczy wiersz z pliku MAP.
    */
    protected class MapLineParser implements LineParser {

        public void parseLine(String line) throws IOException,
            NoSuchElementException
        {
            StringTokenizer tokenizer = new StringTokenizer(line);
            String command = tokenizer.nextToken();

            if (command.equals("v")) {
                // stwórz nowy wierzchołek
                vertices.add(new Vector3D(
                    Float.parseFloat(tokenizer.nextToken()),
                    Float.parseFloat(tokenizer.nextToken()),
                    Float.parseFloat(tokenizer.nextToken())));
            }
            else if (command.equals("mtllib")) {
                // wczytaj materiały z pliku
                String name = tokenizer.nextToken();
                parseFile(name);
            }
            else if (command.equals("usemtl")) {
                // zdefiniuj bieżący materiał
                String name = tokenizer.nextToken();
                if ("null".equals(name)) {
                    currentMaterial = new Material();
                }
                else {
                    currentMaterial =
                        (Material)materials.get(name);
                    if (currentMaterial == null) {
                        currentMaterial = new Material();
                        System.out.println("brak materiału: " + name);
                    }
                }
            }
            else if (command.equals("pointlight")) {
                // stwórz światło punktowe
                Vector3D loc = getVector(tokenizer.nextToken());
                float intensity = 1;
                float falloff = PointLight3D.NO_DISTANCE_FALLOFF;
                if (tokenizer.hasMoreTokens()) {
                    intensity =
                        Float.parseFloat(tokenizer.nextToken());
                }
                if (tokenizer.hasMoreTokens()) {
                    falloff =
                        Float.parseFloat(tokenizer.nextToken());
                }
                lights.add(new PointLight3D(loc.x, loc.y, loc.z,
                    intensity, falloff));
            }
            else if (command.equals("ambientLightIntensity")) {
                // zdefiniuj intensywność światła otaczającego
                ambientLightIntensity =
                    Float.parseFloat(tokenizer.nextToken());
            }
            else if (command.equals("player")) {
                // zdefiniuj początkowe położenie gracza
                playerStart.getLocation().setTo(
                    getVector(tokenizer.nextToken()));
                if (tokenizer.hasMoreTokens()) {
                    playerStart.setAngleY(
                        Float.parseFloat(tokenizer.nextToken()));
                }
            }
            else if (command.equals("obj")) {
                // stwórz nowy obiekt
                String uniqueName = tokenizer.nextToken();
                String filename = tokenizer.nextToken();
                // sprawdź, czy obiekt nie został już wczytany
                PolygonGroup object =
                    (PolygonGroup)loadedObjects.get(filename);
                if (object == null) {
                    File file = new File(path, filename);
                    String filePath = file.getPath();
                    object = objectLoader.loadObject(filePath);
                    loadedObjects.put(filename, object);
                }
                Vector3D loc = getVector(tokenizer.nextToken());
                PolygonGroup mapObject =
                    (PolygonGroup)object.clone();
                mapObject.getTransform().getLocation().setTo(loc);
                if (!uniqueName.equals("null")) {
                    mapObject.setName(uniqueName);
                }
                if (tokenizer.hasMoreTokens()) {
                    mapObject.getTransform().setAngleY(
                        Float.parseFloat(tokenizer.nextToken()));
                }
                mapObjects.add(mapObject);
            }
            else if (command.equals("room")) {
                // rozpocznij wczytywanie nowego pomieszczenia
                currentRoom = new RoomDef(ambientLightIntensity);
                rooms.add(currentRoom);
            }
            else if (command.equals("floor")) {
                // zdefiniuj podłogę w pomieszczeniu
                float y = Float.parseFloat(tokenizer.nextToken());
                currentRoom.setFloor(y, currentMaterial.texture);
            }
            else if (command.equals("ceil")) {
                // zdefiniuj sufit w pomieszczeniu
                float y = Float.parseFloat(tokenizer.nextToken());
                currentRoom.setCeil(y, currentMaterial.texture);
            }
            else if (command.equals("wall")) {
                // zdefiniuj wierzchołek ściany w pomieszczeniu
                float x = Float.parseFloat(tokenizer.nextToken());
                float z = Float.parseFloat(tokenizer.nextToken());
                if (tokenizer.hasMoreTokens()) {
                    float bottom =
                        Float.parseFloat(tokenizer.nextToken());
                    float top =
                        Float.parseFloat(tokenizer.nextToken());
                    currentRoom.addVertex(x, z, bottom, top,
                        currentMaterial.texture);
                }
                else {
                    currentRoom.addVertex(x, z,
                        currentMaterial.texture);
                }
            }
            else {
                System.out.println("Nienana instrukcja: " + command);
            }
        }
    }
}