package com.brackeen.javagamebook.graphics3D;

import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.util.MoreMath;

/**
    Klasa ScanConverter konwertuje rzutowany wielokąt na serię
    poziomuch skanów (linii) rysowanych na ekranie.
*/
public class ScanConverter {

    private static final int SCALE_BITS = 16;
    private static final int SCALE = 1 << SCALE_BITS;
    private static final int SCALE_MASK = SCALE - 1;

    protected ViewWindow view;
    protected Scan[] scans;
    protected int top;
    protected int bottom;

    /**
        Pozioma linia skanu.
    */
    public static class Scan {
        public int left;
        public int right;

        /**
            Wyznacza lewą i prawą granicę tego skanu, jeśli
            wartość x jest na zewnątrz bieżącej granicy.
        */
        public void setBoundary(int x) {
            if (x < left) {
                left = x;
            }
            if (x-1 > right) {
                right = x-1;
            }
        }


        /**
            Oczyszcza tę linię skanu.
        */
        public void clear() {
            left = Integer.MAX_VALUE;
            right = Integer.MIN_VALUE;
        }


        /**
            Ustala, czy skan jest poprawny (tj. czy left <= right).
        */
        public boolean isValid() {
            return (left <= right);
        }


        /**
            Definiuje ten skan.
        */
        public void setTo(int left, int right) {
            this.left = left;
            this.right = right;
        }


        /**
            Sprawdza, czy ten skan jest równy zdefiniowanym wartościom.
        */
        public boolean equals(int left, int right) {
            return (this.left == left && this.right == right);
        }
    }

    /**
        Tworzy nowy ScanConverter dla podanego okna obrazu ViewWindow.
        Właściwości okna ViewWindow mogą się zmieniać pomiędzy kolejnymi
        konwersjami na skany.
    */
    public ScanConverter(ViewWindow view) {
        this.view = view;
    }


    /**
        Pobiera górną granicę ostatniego konwertowanego wielokąta.
    */
    public int getTopBoundary() {
        return top;
    }


    /**
        Pobiera dolną granicę ostatniego konwertowanego 
        wielokąta.
    */
    public int getBottomBoundary() {
        return bottom;
    }


    /**
        Pobiera linię skanu dla określonej wartości y.
    */
    public Scan getScan(int y) {
        return scans[y];
    }


    /**
        Upewnia się, że ScanConverter ma pojemność wystarczającą
        do konwertowania wielokąta dla potrzeb okna ViewWindow.
    */
    protected void ensureCapacity() {
        int height = view.getTopOffset() + view.getHeight();
        if (scans == null || scans.length != height) {
            scans = new Scan[height];
            for (int i=0; i<height; i++) {
                scans[i] = new Scan();
            }
            // definiuje górę (top) i dół (bottom), aby metoda clearCurrentScan mogła wszystko oczyścić
            top = 0;
            bottom = height - 1;
        }

    }


    /**
        Oczyszcza bieżący skan.
    */
    private void clearCurrentScan() {
        for (int i=top; i<=bottom; i++) {
            scans[i].clear();
        }
        top = Integer.MAX_VALUE;
        bottom = Integer.MIN_VALUE;
    }


    /**
        Konwertuje rzutowany wielokąt. Zwraca true, jeśli
        wielokąt jest widoczny w oknie obrazu.
    */
    public boolean convert(Polygon3D polygon) {

        ensureCapacity();
        clearCurrentScan();

        int minX = view.getLeftOffset();
        int maxX = view.getLeftOffset() + view.getWidth() - 1;
        int minY = view.getTopOffset();
        int maxY = view.getTopOffset() + view.getHeight() - 1;

        int numVertices = polygon.getNumVertices();
        for (int i=0; i<numVertices; i++) {
            Vector3D v1 = polygon.getVertex(i);
            Vector3D v2;
            if (i == numVertices - 1) {
                v2 = polygon.getVertex(0);
            }
            else {
                v2 = polygon.getVertex(i+1);
            }

            // upewnij się, że v1.y < v2.y
            if (v1.y > v2.y) {
                Vector3D temp = v1;
                v1 = v2;
                v2 = temp;
            }
            float dy = v2.y - v1.y;

            // ignoruj linie poziome
            if (dy == 0) {
                continue;
            }

            int startY = Math.max(MoreMath.ceil(v1.y), minY);
            int endY = Math.min(MoreMath.ceil(v2.y)-1, maxY);
            top = Math.min(top, startY);
            bottom = Math.max(bottom, endY);
            float dx = v2.x - v1.x;

            // specjalny przypadek: linia pionowa
            if (dx == 0) {
                int x = MoreMath.ceil(v1.x);
                // upewnij się, że x mieści się w granicach obrazu
                x = Math.min(maxX+1, Math.max(x, minX));
                for (int y=startY; y<=endY; y++) {
                    scans[y].setBoundary(x);
                }
            }
            else {
                // wykryj krawędź podczas skanu (używając równania prostej)
                float gradient = dx / dy;

                // (wolniejsza wersja)
                /*
                for (int y=startY; y<=endY; y++) {
                    int x = MoreMath.ceil(v1.x + (y - v1.y) * gradient);
                    // upewnij się, że x jest w obrębie granic obrazu
                    x = Math.min(maxX+1, Math.max(x, minX));
                    scans[y].setBoundary(x);
                }
                */

                // (szybsza wersja)


                // przytnij początek linii
                float startX = v1.x + (startY - v1.y) * gradient;
                if (startX < minX) {
                    int yInt = (int)(v1.y + (minX - v1.x) /
                        gradient);
                    yInt = Math.min(yInt, endY);
                    while (startY <= yInt) {
                        scans[startY].setBoundary(minX);
                        startY++;
                    }
                }
                else if (startX > maxX) {
                    int yInt = (int)(v1.y + (maxX - v1.x) /
                        gradient);
                    yInt = Math.min(yInt, endY);
                    while (startY <= yInt) {
                        scans[startY].setBoundary(maxX+1);
                        startY++;
                    }
                }

                if (startY > endY) {
                    continue;
                }

                // przytnij koniec linii
                float endX = v1.x + (endY - v1.y) * gradient;
                if (endX < minX) {
                    int yInt = MoreMath.ceil(v1.y + (minX - v1.x) /
                        gradient);
                    yInt = Math.max(yInt, startY);
                    while (endY >= yInt) {
                        scans[endY].setBoundary(minX);
                        endY--;
                    }
                }
                else if (endX > maxX) {
                    int yInt = MoreMath.ceil(v1.y + (maxX - v1.x) /
                        gradient);
                    yInt = Math.max(yInt, startY);
                    while (endY >= yInt) {
                        scans[endY].setBoundary(maxX+1);
                        endY--;
                    }
                }

                if (startY > endY) {
                    continue;
                }

                // równanie prostej z wykorzystaniem liczb całkowitych
                int xScaled = (int)(SCALE * v1.x +
                    SCALE * (startY - v1.y) * dx / dy) + SCALE_MASK;
                int dxScaled = (int)(dx * SCALE / dy);

                for (int y=startY; y<=endY; y++) {
                    scans[y].setBoundary(xScaled >> SCALE_BITS);
                    xScaled+=dxScaled;
                }
            }
        }

        // sprawdź, czy widać cokolwiek (czy są jakieś poprawne skany)
        for (int i=top; i<=bottom; i++) {
            if (scans[i].isValid()) {
                return true;
            }
        }
        return false;
    }



}