package com.brackeen.javagamebook.bsp2D;

import java.awt.*;
import java.awt.image.*;
import java.util.HashMap;
import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.bsp2D.*;
import com.brackeen.javagamebook.graphics3D.*;
import com.brackeen.javagamebook.graphics3D.texture.*;
import com.brackeen.javagamebook.game.GameObjectManager;

/**
    Klasa BSPRenderer odpowiada za rednerowanie rysowanych wielokątów,
    które są przechowywane w drzewie BSP oraz wszystkich obiektów złożonych z
    wielokątów. Rysując te wielokąty klasa BSPRenderer zapisuje ich głębości w
    z-buforze. Obiekty złożone z wielokątów wykorzystują z-bufor do określania
    (na poziomie pojedynczych pikseli), czy są widoczne na scenie.
*/
public class BSPRenderer extends ZBufferedRenderer
    implements BSPTreeTraverseListener
{

    /**
        Definiuje, ile wielokątów należy narysować przed sprawdzeniem wypełnienia
        ekranu.
    */
    private static final int FILLED_CHECK = 3;

    protected HashMap bspScanRenderers;
    protected BSPTreeTraverser traverser;
    protected Graphics2D currentGraphics2D;
    protected boolean viewNotFilledFirstTime;
    protected int polygonCount;

    /**
        Tworzy nowy obiekt BSPRenderer dla przekazanego obiektu kamery
        i okna widoku.
    */
    public BSPRenderer(Transform3D camera,
        ViewWindow viewWindow)
    {
        super(camera, viewWindow, false);
        viewNotFilledFirstTime = true;
        traverser = new BSPTreeTraverser(this);
    }


    /**
        Ustawia GamebjectManager. Przeglądanie drzewa BSP pozwala
        określić, które obiekty są widoczne.
    */
    public void setGameObjectManager(
        GameObjectManager gameObjectManager)
    {
        traverser.setGameObjectManager(gameObjectManager);
    }


    protected void init() {
        destPolygon = new TexturedPolygon3D();
        scanConverter = new SortedScanConverter(viewWindow);

        // tworzy obiekty renderujące dla wszystkich tekstur (optymalizacja HotSpot)
        scanRenderers = new HashMap();
        scanRenderers.put(PowerOf2Texture.class,
            new PowerOf2TextureZRenderer());
        scanRenderers.put(ShadedTexture.class,
            new ShadedTextureZRenderer());
        scanRenderers.put(ShadedSurface.class,
            new ShadedSurfaceZRenderer());

        // jw., ale dla wielokątów przechowywanych w drzewie BSP
        bspScanRenderers = new HashMap();
        bspScanRenderers.put(PowerOf2Texture.class,
            new PowerOf2TextureRenderer());
        bspScanRenderers.put(ShadedTexture.class,
            new ShadedTextureRenderer());
        bspScanRenderers.put(ShadedSurface.class,
            new ShadedSurfaceRenderer());
    }


    public void startFrame(Graphics2D g) {
        super.startFrame(g);
        ((SortedScanConverter)scanConverter).clear();
        polygonCount = 0;
    }


    public void endFrame(Graphics2D g) {
        super.endFrame(g);
        if (!((SortedScanConverter)scanConverter).isFilled()) {
            g.drawString("Ekrane nie jest wypełniony", 5,
                viewWindow.getTopOffset() +
                viewWindow.getHeight() - 5);
            if (viewNotFilledFirstTime) {
                viewNotFilledFirstTime = false;
                // wyświetla na konsoli odpowiedni komunikat
                System.out.println("Ekran nie jest w całości wypełniony.");
            }
            // czyści tło
            clearViewEveryFrame = true;
        }
        else {
            clearViewEveryFrame = false;
        }
    }


    /**
        Rysuje widoczne wielokąty z drzewa BSP w oparciu o położenie
        kamery. Wielokąty są rysowane od pierwszego do ostatniego planu.
    */
    public void draw(Graphics2D g, BSPTree tree) {
        ((SortedScanConverter)scanConverter).setSortedMode(true);
        currentGraphics2D = g;
        traverser.traverse(tree, camera.getLocation());
        ((SortedScanConverter)scanConverter).setSortedMode(false);
    }


    // z interfejsu BSPTreeTraverseListener
    public boolean visitPolygon(BSPPolygon poly, boolean isBack) {
        SortedScanConverter scanConverter =
            (SortedScanConverter)this.scanConverter;

        draw(currentGraphics2D, poly);

        // co trzy wielokąty sprawdza, czy ekran jest wypełniony
        polygonCount++;
        if (polygonCount == FILLED_CHECK) {
            polygonCount = 0;
            return
                !((SortedScanConverter)scanConverter).isFilled();
        }
        return true;
    }


    protected void drawCurrentPolygon(Graphics2D g) {
        if (!(sourcePolygon instanceof BSPPolygon)) {
            super.drawCurrentPolygon(g);
            return;
        }
        buildSurface();
        SortedScanConverter scanConverter =
            (SortedScanConverter)this.scanConverter;
        TexturedPolygon3D poly = (TexturedPolygon3D)destPolygon;
        Texture texture = poly.getTexture();
        ScanRenderer scanRenderer = (ScanRenderer)
            bspScanRenderers.get(texture.getClass());
        scanRenderer.setTexture(texture);
        Rectangle3D textureBounds = poly.getTextureBounds();

        a.setToCrossProduct(textureBounds.getDirectionV(),
            textureBounds.getOrigin());
        b.setToCrossProduct(textureBounds.getOrigin(),
            textureBounds.getDirectionU());
        c.setToCrossProduct(textureBounds.getDirectionU(),
            textureBounds.getDirectionV());

        // zmienna w wykorzystywana do obliczania głębokości każdego piksela
        w = SCALE * MIN_DISTANCE * Short.MAX_VALUE /
            (viewWindow.getDistance() *
            c.getDotProduct(textureBounds.getOrigin()));

        int y = scanConverter.getTopBoundary();
        viewPos.y = viewWindow.convertFromScreenYToViewY(y);
        viewPos.z = -viewWindow.getDistance();

        while (y<=scanConverter.getBottomBoundary()) {
            for (int i=0; i<scanConverter.getNumScans(y); i++) {
                ScanConverter.Scan scan =
                    scanConverter.getScan(y, i);

                if (scan.isValid()) {
                    viewPos.x = viewWindow.
                        convertFromScreenXToViewX(scan.left);
                    int offset = (y - viewWindow.getTopOffset()) *
                        viewWindow.getWidth() +
                        (scan.left - viewWindow.getLeftOffset());

                    scanRenderer.render(offset, scan.left,
                        scan.right);
                    setScanDepth(offset, scan.right-scan.left+1);
                }
            }
            y++;
            viewPos.y--;
        }
    }


    /**
        Ustawia z-głębokość dla bieżącej ścieżki wielokąta.
    */
    private void setScanDepth(int offset, int width) {
        float z = c.getDotProduct(viewPos);
        float dz = c.x;
        int depth = (int)(w*z);
        int dDepth = (int)(w*dz);
        short[] depthBuffer = zBuffer.getArray();
        int endOffset = offset + width;

        // ustawiona głębia będzie stała dla wielu podłóg i sufitów
        if (dDepth == 0) {
            short d = (short)(depth >> SCALE_BITS);
            while (offset < endOffset) {
                depthBuffer[offset++] = d;
            }
        }
        else {
            while (offset < endOffset) {
                depthBuffer[offset++] =
                    (short)(depth >> SCALE_BITS);
                depth += dDepth;
            }
        }
    }

}
