package com.brackeen.javagamebook.game;

import java.awt.Rectangle;
import java.awt.Graphics2D;
import java.util.*;
import com.brackeen.javagamebook.math3D.*;

/**
    Klasa GridGameObjectManager jest implementacją interfejsu GameObjectManager,
    w której obiekty GameObject są rozdzielan w dwuwymiarowej siatce, co pozwala
    określić widoczność i ograniczyć liczbę testów mających na celu wykrywanie
    kolizji.
*/
public class GridGameObjectManager implements GameObjectManager {

    /**
        Domyślny rozmiar siatki to 512. Rozmiar powinien być większy dla większych
        obiektów.
    */
    private static final int GRID_SIZE_BITS = 9;
    private static final int GRID_SIZE = 1 << GRID_SIZE_BITS;

    /**
        Klasa Cell reprezentuje komórkę w siatce. Zawiera listę obiektów w grze
        i flagę widoczności.
    */
    private static class Cell {
        List objects;
        boolean visible;

        Cell() {
            objects = new ArrayList();
            visible = false;
        }
    }

    private Cell[] grid;
    private Rectangle mapBounds;
    private int gridWidth;
    private int gridHeight;
    private List allObjects;
    private List spawnedObjects;
    private GameObject player;
    private Vector3D oldLocation;
    private CollisionDetection collisionDetection;

    /**
        Tworzy nowy obiekt klasy GridGameObjectManager z podanymi
        ograniczeniami mapy i obiektem obsługującym wykrywanie kolizji.
        Obiekty GameObjects spoza ograniczeń mapy nie będą nigdy
        wyświetlane.
    */
    public GridGameObjectManager(Rectangle mapBounds,
        CollisionDetection collisionDetection)
    {
        this.mapBounds = mapBounds;
        this.collisionDetection = collisionDetection;
        gridWidth = (mapBounds.width >> GRID_SIZE_BITS) + 1;
        gridHeight = (mapBounds.height >> GRID_SIZE_BITS) + 1;
        grid = new Cell[gridWidth*gridHeight];
        for (int i=0; i<grid.length; i++) {
            grid[i] = new Cell();
        }
        allObjects = new ArrayList();
        spawnedObjects = new ArrayList();
        oldLocation = new Vector3D();
    }


    /**
        Przekształca współrzędne x mapy we współrzędne x siatki.
    */
    private int convertMapXtoGridX(int x) {
        return (x - mapBounds.x) >> GRID_SIZE_BITS;
    }


    /**
        Przekształca współrzędne y mapy we współrzędne y siatki.
    */
    private int convertMapYtoGridY(int y) {
        return (y - mapBounds.y) >> GRID_SIZE_BITS;
    }


    /**
        Oznacza wszystkie obiekty jako potencjalnie widoczne (czyli takie,
        które należy narysować).
    */
    public void markAllVisible() {
        for (int i=0; i<grid.length; i++) {
            grid[i].visible = true;
        }
    }


    /**
        Oznacza wszystkie obiekty wewnątrz przekazanego obszaru 2D jako
        potencjalnie widoczne (czyli takie, które należy narysować).
    */
    public void markVisible(Rectangle bounds) {
        int x1 = Math.max(0, convertMapXtoGridX(bounds.x));
        int y1 = Math.max(0, convertMapYtoGridY(bounds.y));
        int x2 = Math.min(gridWidth-1,
            convertMapXtoGridX(bounds.x + bounds.width));
        int y2 = Math.min(gridHeight-1,
            convertMapYtoGridY(bounds.y + bounds.height));

        for (int y=y1; y<=y2; y++) {
            int offset = y * gridWidth;
            for (int x=x1; x<=x2; x++) {
                grid[offset+x].visible = true;
            }
        }
    }

    public Iterator iterator() {
        return allObjects.iterator();
    }

    /**
        Dodaje obiekt GameObject do tego menadżera.
    */
    public void add(GameObject object) {
        if (object != null) {
            if (object == player) {
                // zapewnia, że ruch gracza następuje jako pierwszy
                allObjects.add(0, object);
            }
            else {
                allObjects.add(object);
            }
            Cell cell = getCell(object);
            if (cell != null) {
                cell.objects.add(object);
            }

        }
    }


    /**
        Usuwa obiekt GameObject z tego menadżera.
    */
    public void remove(GameObject object) {
        if (object != null) {
            allObjects.remove(object);
            Cell cell = getCell(object);
            if (cell != null) {
                cell.objects.remove(object);
            }
        }
    }


    /**
        Dodaje obiekt GameObject do tego menadżera, zaznaczając, że
        jest to obiekt gracza. Istniejący obiekt gracza, jeśli występuje,
        nie jest usuwany.
    */
    public void addPlayer(GameObject player) {
        this.player = player;
        if (player != null) {
            player.getListener().notifyVisible(player, true);
            add(player);
        }
    }


    /**
        Zwraca obiekt oznaczony jako obiekt gracza lub null,
        jeśli nie wskazano obiektu gracza.
    */
    public GameObject getPlayer() {
        return player;
    }


    /**
        Zwraca komórkę, w której znajduje się podany obiekt GameObject, lub
        null, jeśli ten obiekt GameObject nie mieści się w granicach mapy.
    */
    private Cell getCell(GameObject object) {
        int x = convertMapXtoGridX((int)object.getX());
        int y = convertMapYtoGridY((int)object.getZ());
        return getCell(x, y);
    }


    /**
        Zwraca komórkę, w której znajduje się podany punkt, lub
        null, jeśli współrzędne punktu nie są prawidłowe.
    */
    private Cell getCell(int x, int y) {

        // sprawdza ograniczenia
        if (x < 0 || y < 0 || x >= gridWidth || y >= gridHeight) {
            return null;
        }

        // zwraca komórkę, do której należy punkt o współrzędnych x, y
        return grid[x + y * gridWidth];
    }


    /**
        Aktualizuje wszystkie obiektu na podstawie czasu, który upłynął
        od ostatniej aktualizacji i zastosowania mechanizmu wykrywania
        kolizji.
    */
    public void update(long elapsedTime) {
        for (int i=0; i<allObjects.size(); i++) {
            GameObject object = (GameObject)allObjects.get(i);

            // zapisz starą pozycję obiektu
            Cell oldCell = getCell(object);
            oldLocation.setTo(object.getLocation());
            boolean isRegenerating = false;

            // przesuń obiekt
            object.update(player, elapsedTime);

            // śledź wszystkie nowe obiekty (dodawane za chwilę)
            List spawns = object.getSpawns();
            if (spawns != null) {
                if (spawns.contains(object)) {
                    isRegenerating = true;
                }
                spawnedObjects.addAll(spawns);
            }

            // jeśli obiekt został zniszczony, usuń go
            if (object.isDestroyed() || isRegenerating) {
                allObjects.remove(i);
                i--;
                if (oldCell != null) {
                    oldCell.objects.remove(object);
                }
                continue;
            }

            // jeśli obiekt wykonał ruch, wykonaj procedurę wykrywania kolizji
            if (!object.getLocation().equals(oldLocation) ||
                object.isJumping())
            {

                // sprawdź ściany, podłogi i sufity
                collisionDetection.checkBSP(object,
                    oldLocation, elapsedTime);

                // sprawdź pozostałe obiekty
                if (checkObjectCollision(object, oldLocation)) {
                    // revert to old position
                    object.getLocation().setTo(oldLocation);
                }

                // zaktualizuj położenie w siatce
                Cell cell = getCell(object);
                if (cell != oldCell) {
                    if (oldCell != null) {
                        oldCell.objects.remove(object);
                    }
                    if (cell != null) {
                        cell.objects.add(object);
                    }
                }
               object.sendTouchNotifications();
            }
        }

        // dodaj wszystkie nowe obiekty
        if (spawnedObjects.size() > 0) {
            for (int i=0; i<spawnedObjects.size(); i++) {
                add((GameObject)spawnedObjects.get(i));
            }
            spawnedObjects.clear();
        }
    }


    /**
        Sprawdza, czy dany obiekt koliduje z którymkolwiek z pozostałych
        obiektów.
    */
    public boolean checkObjectCollision(GameObject object,
        Vector3D oldLocation)
    {

        boolean collision = false;

        // użyj współrzędnych (x,z) dla obiektu (pozycja w poziomie)
        int x = convertMapXtoGridX((int)object.getX());
        int y = convertMapYtoGridY((int)object.getZ());

        // sprawdź 9 komórek otaczających obiekt
        for (int i=x-1; i<=x+1; i++) {
            for (int j=y-1; j<=y+1; j++) {
                Cell cell = getCell(i, j);
                if (cell != null) {
                    collision |= collisionDetection.checkObject(
                        object, cell.objects, oldLocation);
                }
            }
        }

        return collision;
    }


    /**
        DNarysuj wszystkie widoczne obiekty i oznacz wszystkei obiekt,
        które nie są widoczne.
    */
    public void draw(Graphics2D g, GameObjectRenderer r) {
        for (int i=0; i<grid.length; i++) {
            List objects = grid[i].objects;
            for (int j=0; j<objects.size(); j++) {
                GameObject object = (GameObject)objects.get(j);
                boolean visible = false;
                if (grid[i].visible) {
                    visible = r.draw(g, object);
                }
                if (object != player) {
                    // poinformuj obiekty, które są widoczne
                    object.getListener().notifyVisible(object, visible);
                }
            }
            grid[i].visible = false;
        }
    }
}
