package com.brackeen.javagamebook.math3D;

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

/**
    Klasa ObjectLoader wczytuje podzbiór opracowanej przez
    Alias|Wavefront specyfikacji plików OBJ.

    Wiersze zaczynające się od znaku '#' są komentarzami.

    Słowa kluczow pliku OBJ:
    <pre>
    mtllib [filename]    - Ładuje materiały z zewnętrznego pliku
                           .mtl.
    v [x] [y] [z]        - Definiuje wierzchołek opisany współrzędnymi
                           zmiennoprzecinkowymi (x,y,z).
    f [v1] [v2] [v3] ... - Definiuje nowy bok (face). Bok to płaski, wypukły
                           wielokąt z wierzchołkami ustawionymi w porządku
                           odwrotnym do ruchu wskazówek zegara. Liczby 
                           dodatnie wskazują wierzchołek, który został
                           zdefiniowany w pliku. 
                           Liczby ujemne definiują wierzchołki według 
                           ich kolejności wczytywania.
                           Na przykład, 1 oznacza pierwszy wierzchołek 
                           w pliku, -1 oznacza ostatnio wczytany 
                           wierzchołek, a -2 przedostatni wczytany
                           wierzchołek.
    g [name]             - Definiuje nową nazwę grupy. Boki są następnie
                           dodawane do tej grupy.
    usemtl [name]        - wykorzystuje podany materiał (załadowany z pliku
                           .mtl) do wylorzystania w bokach tej grupy.
    </pre>

    MTL file keywords:
    <pre>
    newmtl [name]        - Definiuje nowy materiał podając nazwę.
    map_Kd [filename]    - Przypisuje materiałowi mapę tekstury.
    </pre>
*/
public class ObjectLoader {

    /**
        Klasa Material owija ShadedTexture.
    */
    public static class Material {
        public File sourceFile;
        public ShadedTexture texture;
    }


    /**
        LineParser jest interfejsem parsera wiersza z pliku tekstowego.
        Do przetwarzania plików OBJ i MTL będziemy używać osobnych
        LineParserów.
    */
    protected interface LineParser {
        public void parseLine(String line) throws IOException, NumberFormatException, NoSuchElementException;
    }

    protected File path;
    protected List vertices;
    protected Material currentMaterial;
    protected HashMap materials;
    protected List lights;
    protected float ambientLightIntensity;
    protected HashMap parsers;
    private PolygonGroup object;
    private PolygonGroup currentGroup;

    /**
        Tworzy nowy ObjectLoader.
    */
    public ObjectLoader() {
        materials = new HashMap();
        vertices = new ArrayList();
        parsers = new HashMap();
        parsers.put("obj", new ObjLineParser());
        parsers.put("mtl", new MtlLineParser());
        currentMaterial = null;
        setLights(new ArrayList(), 1);
    }

    /**
        Ustawia światłą dla wielokątów tworzących przetwarzane obiekty.
        Po wywołaniu tej metody, wywołania metody loadObject używają
        tych świateł.
    */
    public void setLights(List lights,
        float ambientLightIntensity)
    {
        this.lights = lights;
        this.ambientLightIntensity = ambientLightIntensity;
    }


    /**
        Wczytuje plik OBJ jako PolygonGroup.
    */
    public PolygonGroup loadObject(String filename)
        throws IOException
    {
        File file = new File(filename);
        object = new PolygonGroup();
        object.setFilename(file.getName());
        path = file.getParentFile();

        vertices.clear();
        currentGroup = object;
        parseFile(filename);

        return object;
    }


    /**
        Zwraca Vector3D z listy wektorów w pliku. Indeksy ujemne
        są liczone od końca listy; indeksy dodatnie wskazują na
        wektory od początku listy. 1 jest pierwszym indeksem,
        -1 jest ostatnim indeksem. 0 jest nieprawidłowym indeksem
        i powoduje wygenerowanie wyjątku.
    */
    protected Vector3D getVector(String indexStr) {
        int index = Integer.parseInt(indexStr);
        if (index < 0) {
            index = vertices.size() + index + 1;
        }
        return (Vector3D)vertices.get(index-1);
    }


    /**
        Parsuje plik OBJ (z rozszerzeniem ".obj") lub plik MTL
        (z rozszerzeniem ".mtl").
    */
    protected void parseFile(String filename) throws IOException
    {
        // otwórz plik o podanej ścieżce
        File file = new File(path, filename);
        BufferedReader reader = new BufferedReader(new FileReader(file));

        // uruchom parser zgodnie z rozszerzeniem pliku
        LineParser parser = null;
        int extIndex = filename.lastIndexOf('.');
        if (extIndex != -1) {
            String ext = filename.substring(extIndex+1);
            parser = (LineParser)parsers.get(ext.toLowerCase());
        }
        if (parser == null) {
            parser = (LineParser)parsers.get("obj");
        }

        // parsuj wszystkie wiersze w pliku
        while (true) {
            String line = reader.readLine();
            // brak kolejnych wierszy do wczytania
            if (line == null) {
                reader.close();
                return;
            }

            line = line.trim();

            // ignoruj puste wiersze i komentarze
            if (line.length() > 0 && !line.startsWith("#")) {
                // interpretuj wiersz
                try {
                    parser.parseLine(line);
                }
                catch (NumberFormatException ex) {
                    throw new IOException(ex.getMessage());
                }
                catch (NoSuchElementException ex) {
                    throw new IOException(ex.getMessage());
                }
            }

        }
    }

    /**
        Parsuje wiersz z pliku OBJ.
    */
    protected class ObjLineParser implements LineParser {

        public void parseLine(String line) throws IOException,
            NumberFormatException, NoSuchElementException
        {
            StringTokenizer tokenizer = new StringTokenizer(line);
            String command = tokenizer.nextToken();
            if (command.equals("v")) {
                // tworzy nowy wierzchołek
                vertices.add(new Vector3D(Float.parseFloat(tokenizer.nextToken()), Float.parseFloat(tokenizer.nextToken()), Float.parseFloat(tokenizer.nextToken())));
            }
            else if (command.equals("f")) {
                // tworzy nową ścianę (płaski, wypukły wielokąt)
                List currVertices = new ArrayList();
                while (tokenizer.hasMoreTokens()) {
                    String indexStr = tokenizer.nextToken();

                    // ignoruje tekstury i dodatkowe współrzędne
                    int endIndex = indexStr.indexOf('/');
                    if (endIndex != -1) {
                        indexStr = indexStr.substring(0, endIndex);
                    }

                    currVertices.add(getVector(indexStr));
                }

                // tworzy wielokąt pokryty teksturą
                Vector3D[] array = new Vector3D[currVertices.size()];
                currVertices.toArray(array);
                TexturedPolygon3D poly = new TexturedPolygon3D(array);

                // ustawia teksturę
                ShadedSurface.createShadedSurface(poly, currentMaterial.texture, lights,  ambientLightIntensity);

                // dodaje wielokąt do bieżącej grupy
                currentGroup.addPolygon(poly);
            }
            else if (command.equals("g")) {
                // definiuje bieżącą grupę
                if (tokenizer.hasMoreTokens()) {
                    String name = tokenizer.nextToken();
                    currentGroup = new PolygonGroup(name);
                }
                else {
                    currentGroup = new PolygonGroup();
                }
                object.addPolygonGroup(currentGroup);
            }
            else if (command.equals("mtllib")) {
                // wczytuje materiały z pliku
                String name = tokenizer.nextToken();
                parseFile(name);
            }
            else if (command.equals("usemtl")) {
                // definiuje bieżący materiał
                String name = tokenizer.nextToken();
                currentMaterial = (Material)materials.get(name);
                if (currentMaterial == null) {
                    System.out.println("brak materiału: " + name);
                }
            }
            else {
                // nieznane polecenie - ignoruję je
            }

        }
    }


    /**
        Parsuje pojedynczy wiersz pliku materiałów MTL.
    */
    protected class MtlLineParser implements LineParser {

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

            if (command.equals("newmtl")) {
                // w razie potrzeby tworzy nowy materiał
                String name = tokenizer.nextToken();
                currentMaterial = (Material)materials.get(name);
                if (currentMaterial == null) {
                    currentMaterial = new Material();
                    materials.put(name, currentMaterial);
                }
            }
            else if (command.equals("map_Kd")) {
                // nadaje bieżącemu materiałowi teksturę
                String name = tokenizer.nextToken();
                File file = new File(path, name);
                if (!file.equals(currentMaterial.sourceFile)) {
                    currentMaterial.sourceFile = file;
                    currentMaterial.texture = (ShadedTexture) Texture.createTexture(file.getPath(),true);
                }
            }
            else {
                // nieznana instrukcja - ignoruję ją
            }
        }
    }
}

