package com.brackeen.javagamebook.graphics3D;

import com.brackeen.javagamebook.math3D.*;

/**
    Klasę ScanConverter wykorzystywaliśmy do rysowania wielokątów
    posortowanych od przodu do tyłu (bez nakładania się pikseli). 
    Wielokąty są dodawane do listy i przycinane w sposób umożliwiający
    ich dopasowanie do wielokątów już wyświetlonych. Przed narysowaniem
    każdej klatki należy wywołać metodę clear().
*/
public class SortedScanConverter extends ScanConverter {

    protected static final int DEFAULT_SCANLIST_CAPACITY = 8;

    private SortedScanList[] viewScans;
    private SortedScanList[] polygonScans;
    private boolean sortedMode;

    /**
        Tworzy nowy obiekt klasy SortedScanConverter dla danego okna
        (obiektu klasy ViewWindow). Własności obiektu ViewWindow mogą
        się zmieniać pomiędzy operacjami konwersji ścieżek. Tryb
        sortowania jest domyślnie wyłączony, można go jednak w każdej
        chwili zmienić wywołując metodę setSortedMode().
    */
    public SortedScanConverter(ViewWindow view) {
        super(view);
        sortedMode = false;
    }


    /**
        Czyści bieżącą ścieżkę. Wywołuj tę metodę przed przystąpieniem
        do rysowania każdej klatki.
    */
    public void clear() {
        if (viewScans != null) {
            for (int y=0; y<viewScans.length; y++) {
                viewScans[y].clear();
            }
        }
    }


    /**
        Ustawia tryb sortowanie, co oznacza, że ten konwerter ścieżek
        może zakładać, że wielokąty są rysowane od przodu do tyłu i
        powinny być przycinane w taki sposób, by pasowały do wielokątów
        wcześniej przekonwertowanych na ścieżki.
    */
    public void setSortedMode(boolean b) {
        sortedMode = b;
    }


    /**
        Zwraca n-tą ścieżkę danego rzędu.
    */
    public Scan getScan(int y, int index) {
        return polygonScans[y].getScan(index);
    }


    /**
        Zwraca liczbę ścieżek w danym rzędzie.
    */
    public int getNumScans(int y) {
        return polygonScans[y].getNumScans();
    }


    /**
        Sprawdza, czy ekran jest wypełniony.
    */
    public boolean isFilled() {
        if (viewScans == null) {
            return false;
        }

        int left = view.getLeftOffset();
        int right = left + view.getWidth() - 1;
        for (int y=view.getTopOffset(); y<viewScans.length; y++) {
            if (!viewScans[y].equals(left, right)) {
                return false;
            }
        }
        return true;
    }


    protected void ensureCapacity() {
        super.ensureCapacity();
        int height = view.getTopOffset() + view.getHeight();
        int oldHeight = (viewScans == null)?0:viewScans.length;
        if (height != oldHeight) {
            SortedScanList[] newViewScans =
                new SortedScanList[height];
            SortedScanList[] newPolygonScans =
                new SortedScanList[height];
            if (oldHeight != 0) {
                System.arraycopy(viewScans, 0, newViewScans, 0,
                    Math.min(height, oldHeight));
                System.arraycopy(polygonScans, 0, newPolygonScans,
                    0, Math.min(height, oldHeight));
            }
            viewScans = newViewScans;
            polygonScans = newPolygonScans;
            for (int i=oldHeight; i<height; i++) {
                viewScans[i] = new SortedScanList();
                polygonScans[i] = new SortedScanList();
            }
        }
    }


    /**
        Przekształca wielokąt na ścieżki i — jeśli tryb sortedMode jest
        włączony - dodaje ścieżki do listy i odpowiednio je przycina, by
        piksele nowego wielokąta nie przykrywały pikseli już istniejących.
    */
    public boolean convert(Polygon3D polygon) {
        boolean visible = super.convert(polygon);
        if (!sortedMode || !visible) {
            return visible;
        }

        // przytnij ścieżkę tak, by nie zasłaniała wcześniej dodanych punktów
        visible = false;
        for (int y=getTopBoundary(); y<=getBottomBoundary(); y++) {
            Scan scan = getScan(y);
            SortedScanList diff = polygonScans[y];
            diff.clear();
            if (scan.isValid()) {
                viewScans[y].add(scan.left, scan.right, diff);
                visible |= (polygonScans[y].getNumScans() > 0);
            }
        }

        return visible;

    }


    /**
        Klasa SortedScanList reprezentuje szereg ścieżek dla poziomego
        rzędu pikseli. Nowe ścieżki mogą być dodawane i przycinane w taki
        sposób, by nie zasłaniały istniejących wcześniej dodanych ścieżek.
    */
    private static class SortedScanList {
    
        private int length;
        private Scan[] scans;
    
        /**
            Tworzy nowy obiekt klasy SortedScanList o domyślnej pojemności
            (liczbie ścieżek na poziomy rząd pikseli).
        */
        public SortedScanList() {
            this(DEFAULT_SCANLIST_CAPACITY);
        }
    
    
        /**
            Tworzy nowy obiekt klasy SortedScanList o podanej pojemności
            (liczbie ścieżek na poziomy rząd pikseli).
        */
        public SortedScanList(int capacity) {
            scans = new Scan[capacity];
            for (int i=0; i<capacity; i++) {
                scans[i] = new Scan();
            }
            length = 0;
        }
    
    
        /**
            Czyści tę listę ścieżek.
        */
        public void clear() {
            length = 0;
        }
    
    
        /**
            Zwraca liczbę ścieżek na tej liście.
        */
        public int getNumScans() {
            return length;
        }
    
    
        /**
            Zwraca n-tą ścieżkę z tej listy.
        */
        public Scan getScan(int index) {
            return scans[index];
        }
    
    
        /**
            Sprawdza, czy ta lista ścieżek zawiera tylko jedną ścieżkę i czy ta
            ścieżka jest równa przekazanym wartościom argumentów left i right.
        */
        public boolean equals(int left, int right) {
            return (length == 1 && scans[0].equals(left,right));
        }
    
    
        /**
            Dodaje i przycina nową ścieżkę w rzędzie, umieszcza widoczny
            fragment (różnicę) w określonym obiekcie klasy SortedScanList.
        */
        public void add(int left, int right, SortedScanList diff) {
            for (int i=0; i<length && left <= right; i++) {
                Scan scan = scans[i];
                int maxRight = scan.left - 1;
                if (left <= maxRight) {
                    if (right < maxRight) {
                        diff.add(left, right);
                        insert(left, right, i);
                        return;
                    }
                    else {
                        diff.add(left, maxRight);
                        scan.left = left;
                        left = scan.right + 1;
                        if (merge(i)) {
                            i--;
                        }
                    }
                }
                else if (left <= scan.right) {
                    left = scan.right + 1;
                }
            }
            if (left <= right) {
                insert(left, right, length);
                diff.add(left, right);
            }
    
        }
    
        // metody pomocnicze wykorzystywane przez metodę add()
    
        private void growCapacity() {
            int capacity = scans.length;
            int newCapacity = capacity*2;
            Scan[] newScans = new Scan[newCapacity];
            System.arraycopy(scans, 0, newScans, 0, capacity);
            for (int i=length; i<newCapacity; i++) {
                newScans[i] = new Scan();
            }
            scans = newScans;
        }
    
        private void add(int left, int right) {
            if (length == scans.length) {
                growCapacity();
            }
            scans[length].setTo(left, right);
            length++;
        }
    
        private void insert(int left, int right, int index) {
            if (index > 0) {
                Scan prevScan = scans[index-1];
                if (prevScan.right == left - 1) {
                    prevScan.right = right;
                    return;
                }
            }
    
            if (length == scans.length) {
                growCapacity();
            }
            Scan last = scans[length];
            last.setTo(left, right);
            for (int i=length; i>index; i--) {
                scans[i] = scans[i-1];
            }
            scans[index] = last;
            length++;
        }
    
        private void remove(int index) {
            Scan removed = scans[index];
            for (int i=index; i<length-1; i++) {
                scans[i] = scans[i+1];
            }
            scans[length-1] = removed;
            length--;
        }
    
        private boolean merge(int index) {
            if (index > 0) {
                Scan prevScan = scans[index-1];
                Scan thisScan = scans[index];
                if (prevScan.right == thisScan.left-1) {
                    prevScan.right = thisScan.right;
                    remove(index);
                    return true;
                }
            }
            return false;
        }

    }

}
