package electrosphere.game.client.cells;

import electrosphere.game.client.terrain.manager.ClientTerrainManager;
import electrosphere.game.server.terrain.manager.ServerTerrainManager;
import electrosphere.game.client.world.ClientWorldData;
import electrosphere.main.Globals;
import electrosphere.net.parser.net.message.WorldMessage;
import electrosphere.renderer.ShaderProgram;
import java.util.Arrays;
import java.util.HashMap;
import org.joml.Vector3d;
import org.joml.Vector3f;

/**
 *
 * @author satellite
 */
public class DrawCellManager {
    
    
    //the center of this cell manager's array in cell space
    int cellX;
    int cellY;
    
    
    //the dimensions of the world that this cell manager can handles
    int cellWidth;
    int cellHeight;
    
    //the width of a minicell in this manager
    int miniCellWidth;
    
    //all currently displaying mini cells
    DrawCell[][] cells;
    boolean[][] valid;
    boolean[][] drawable;
    boolean[][] updateable;
    boolean[][] hasRequested;
    boolean[][] needsPhysics;
    boolean[][] hasPhysics;
    
    
    ShaderProgram program;
    
    
    
    int drawRadius = 5;
    int drawStepdownInterval = 3;
    int drawStepdownValue = 25;
    
    int physicsRadius = 3;
    
    int worldBoundDiscreteMin = 0;
    int worldBoundDiscreteMax = 0;
    
    //metadata about the game world
    ClientWorldData clientWorldData;
    
    //client terrain manager
    ClientTerrainManager clientTerrainManager;
    
    
    //ready to start updating?
    boolean update = false;
    
    
    
    public DrawCellManager(ClientWorldData clientWorldData, ClientTerrainManager clientTerrainManager, int discreteX, int discreteY){
        this.clientWorldData = clientWorldData;
        worldBoundDiscreteMax = (int)(this.clientWorldData.getWorldBoundMax().x / this.clientWorldData.getDynamicInterpolationRatio() * 1.0f);
        this.clientTerrainManager = clientTerrainManager;
        this.miniCellWidth = miniCellWidth;
        cells = new DrawCell[drawRadius * 2 + 1][drawRadius * 2 + 1];
        valid = new boolean[drawRadius * 2 + 1][drawRadius * 2 + 1];
        drawable = new boolean[drawRadius * 2 + 1][drawRadius * 2 + 1];
        updateable = new boolean[drawRadius * 2 + 1][drawRadius * 2 + 1];
        hasRequested = new boolean[drawRadius * 2 + 1][drawRadius * 2 + 1];
        needsPhysics = new boolean[physicsRadius * 2 + 1][physicsRadius * 2 + 1];
        hasPhysics = new boolean[physicsRadius * 2 + 1][physicsRadius * 2 + 1];
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                valid[x][y] = false;
                drawable[x][y] = false;
                updateable[x][y] = false;
                hasRequested[x][y] = false;
            }
        }
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            for(int y = 0; y < physicsRadius * 2 + 1; y++){
                needsPhysics[x][y] = true;
                hasPhysics[x][y] = false;
            }
        }
        cellX = discreteX;
        cellY = discreteY;
        
        program = Globals.terrainShaderProgram;
        
        drawRadius = Globals.userSettings.getGraphicsPerformanceLODChunkRadius();
        drawStepdownInterval = Globals.userSettings.getGameplayPhysicsCellRadius();
        physicsRadius = Globals.userSettings.getGameplayPhysicsCellRadius();
        
        update = true;
    }
    
    DrawCellManager(){
        
    }
    
    public int getCellX(){
        return cellX;
    }
    
    public int getCellY(){
        return cellY;
    }
    
    public void setCellX(int x){
        cellX = x;
    }
    
    public void setCellY(int y){
        cellY = y;
    }
    
    void updateInvalidCell(){
        int targetX = 0;
        int targetY = 0;
        boolean found = false;
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            targetX = x;
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                targetY = y;
                if(!valid[x][y]){
                    found = true;
                    break;
                }
            }
            if(found){
                break;
            }
        }

        if(found){
            int currentCellX = cellX - drawRadius + targetX;
            int currentCellY = cellY - drawRadius + targetY;
            
            
            if(
                    currentCellX >= 0 &&
                    currentCellX < clientWorldData.getWorldDiscreteSize() &&
                    currentCellY >= 0 &&
                    currentCellY < clientWorldData.getWorldDiscreteSize()
                    ){
                if(clientTerrainManager.containsHeightmapAtDiscretePoint(currentCellX, currentCellY)){
                    cells[targetX][targetY] = DrawCell.generateTerrainCell(
                            currentCellX,
                            currentCellY,
                            clientTerrainManager.getHeightmapAtPoint(currentCellX, currentCellY),
                            clientTerrainManager.getTextureMapAtPoint(currentCellX, currentCellY),
                            clientWorldData.getDynamicInterpolationRatio(),
                            program
                    );
                    valid[targetX][targetY] = true;
                    drawable[targetX][targetY] = false;
                    updateable[targetX][targetY] = false;
                    hasRequested[targetX][targetY] = false;
//                    if(Math.abs(physicsRadius + 1 - targetX) < physicsRadius && Math.abs(physicsRadius + 1 - targetY) < physicsRadius){
//                        needsPhysics[targetX][targetY] = true;
//                    }
                } else {
                    if(hasRequested[targetX][targetY] == false){
                        //client should request macro values from server
                        Globals.clientConnection.queueOutgoingMessage(WorldMessage.constructRequestChunkMessage(currentCellX, currentCellY));
                        hasRequested[targetX][targetY] = true;
                    }
                }
            } else {
                valid[targetX][targetY] = true;
                drawable[targetX][targetY] = false;
                updateable[targetX][targetY] = false;
                hasRequested[targetX][targetY] = false;
            }
        }
    }
    
    void makeCellDrawable(){
        int targetX = 0;
        int targetY = 0;
        boolean found = false;
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            targetX = x;
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                targetY = y;
                if(valid[x][y] && !drawable[x][y]){
                    found = true;
                    break;
                }
            }
            if(found){
                break;
            }
        }
        
        if(found){
            int currentCellX = cellX - drawRadius + targetX;
            int currentCellY = cellY - drawRadius + targetY;
            if(
                    currentCellX >= 0 &&
                    currentCellX < clientWorldData.getWorldDiscreteSize() &&
                    currentCellY >= 0 &&
                    currentCellY < clientWorldData.getWorldDiscreteSize()
                    ){
                //physics radius calculation
//                if(Math.abs(physicsRadius + 1 - targetX) < physicsRadius && Math.abs(physicsRadius + 1 - targetY) < physicsRadius){
//                    needsPhysics[targetX][targetY] = true;
//                }
                //calculation for stride
                int dist = (int)Math.sqrt((targetX - drawRadius)*(targetX - drawRadius) + (targetY - drawRadius) * (targetY - drawRadius));//Math.abs(targetX - drawRadius) * Math.abs(targetY - drawRadius);
                int stride = Math.min(clientWorldData.getDynamicInterpolationRatio()/2, Math.max(1, dist / drawStepdownInterval * drawStepdownValue));
                while(clientWorldData.getDynamicInterpolationRatio() % stride != 0){
                    stride = stride + 1;
                }
                //make drawable entity
                cells[targetX][targetY].generateDrawableEntity(stride);
            }
            drawable[targetX][targetY] = true;
        }
    }
    
    void updateCellModel(){
        int targetX = 0;
        int targetY = 0;
        boolean found = false;
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            targetX = x;
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                targetY = y;
                if(updateable[x][y]){
                    found = true;
                    break;
                }
            }
            if(found){
                break;
            }
        }
        
        if(found){
            int currentCellX = cellX - drawRadius + targetX;
            int currentCellY = cellY - drawRadius + targetY;
            if(
                    currentCellX >= 0 &&
                    currentCellX < clientWorldData.getWorldDiscreteSize() &&
                    currentCellY >= 0 &&
                    currentCellY < clientWorldData.getWorldDiscreteSize()
                    ){
//                if(Math.abs(drawRadius + 1 - targetX) < physicsRadius && Math.abs(drawRadius + 1 - targetY) < physicsRadius){
//                    needsPhysics[targetX][targetY] = true;
//                }
                int dist = (int)Math.sqrt((targetX - drawRadius)*(targetX - drawRadius) + (targetY - drawRadius) * (targetY - drawRadius)); //Math.abs(targetX - drawRadius) * Math.abs(targetY - drawRadius);
                int stride = Math.min(clientWorldData.getDynamicInterpolationRatio()/2, Math.max(1, dist / drawStepdownInterval * drawStepdownValue));
                while(clientWorldData.getDynamicInterpolationRatio() % stride != 0){
                    stride = stride + 1;
                }
                cells[targetX][targetY].generateDrawableEntity(stride);
            }
            updateable[targetX][targetY] = false;
            drawable[targetX][targetY] = true;
        }
    }
    
    
    public boolean containsInvalidCell(){
//        if(DRAW_CELL_MANAGER_FLAG_UPDATE_DYNAMIC_TERRAIN){
            for(int x = 0;x < drawRadius * 2 + 1; x++){
                for(int y = 0; y < drawRadius * 2 + 1; y++){
                    if(!valid[x][y]){
                        return true;
                    }
                }
            }
//        } else if(DRAW_CELL_MANAGER_FLAG_GENERATE_ARENA){
//            return cells[0][0] == null || cells[1][0] == null || cells[0][1] == null | cells[1][1] == null;
//        }
        return false;
    }
    
    public boolean containsUndrawableCell(){
        for(int x = 0;x < drawRadius * 2 + 1; x++){
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                if(!drawable[x][y]){
                    return true;
                }
            }
        }
        return false;
    }
    
    public boolean containsUpdateableCell(){
        for(int x = 0;x < drawRadius * 2 + 1; x++){
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                if(updateable[x][y]){
                    return true;
                }
            }
        }
        return false;
    }
    
    public boolean containsPhysicsNeedingCell(){
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            for(int y = 0; y < physicsRadius * 2 + 1; y++){
                if(needsPhysics[x][y]){
                    return true;
                }
            }
        }
        return false;
    }
    
    public void addPhysicsToCell(){
        int targetX = 0;
        int targetY = 0;
        boolean found = false;
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            targetX = x;
            for(int y = 0; y < physicsRadius * 2 + 1; y++){
                targetY = y;
//                System.out.println(x + " <=>w " + y);
                if(needsPhysics[x][y]){
                    found = true;
                    break;
                }
            }
            if(found){
                break;
            }
        }
        
        if(found){
            int currentCellX = cellX - physicsRadius + targetX;
            int currentCellY = cellY - physicsRadius + targetY;
            if(
                    currentCellX >= 0 &&
                    currentCellX < clientWorldData.getWorldDiscreteSize() &&
                    currentCellY >= 0 &&
                    currentCellY < clientWorldData.getWorldDiscreteSize()
                    ){
//                if(Math.abs(drawRadius + 1 - targetX) < physicsRadius && Math.abs(drawRadius + 1 - targetY) < physicsRadius){
//                    needsPhysics[targetX][targetY] = true;
//                }
//                int dist = (int)Math.sqrt((targetX - drawRadius)*(targetX - drawRadius) + (targetY - drawRadius) * (targetY - drawRadius)); //Math.abs(targetX - drawRadius) * Math.abs(targetY - drawRadius);
//                int stride = Math.min(clientWorldData.getDynamicInterpolationRatio()/2, Math.max(1, dist / drawStepdownInterval * drawStepdownValue));
//                while(clientWorldData.getDynamicInterpolationRatio() % stride != 0){
//                    stride = stride + 1;
//                }
//                if(cells[targetX][targetY + drawRadius] != null){
//                    System.out.println(targetX + " - " + targetY);
                    cells[targetX + drawRadius - physicsRadius][targetY + drawRadius - physicsRadius].generatePhysics();
//                } else {
//                    System.out.println("Current cell is null: " + currentCellX + " - " + currentCellY);
//                }
            }
            needsPhysics[targetX][targetY] = false;
            hasPhysics[targetX][targetY] = true;
        }
    }
    
    
    public void shiftChunksNegX(){
        //retire old graphics
        for(int y = 0; y < drawRadius * 2 + 1; y++){
            if(cells[drawRadius * 2][y] != null){
                cells[drawRadius * 2][y].retireCell();
            }
        }
        //shift draw array
        for(int x = drawRadius * 2; x > 0; x--){
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                cells[x][y] = cells[x-1][y];
                updateable[x][y] = true;
                hasRequested[x][y] = hasRequested[x-1][y];
            }
        }
        //invalidate edge of draw array
        for(int y = 0; y < drawRadius * 2 + 1; y++){
            valid[0][y] = false;
            hasRequested[0][y] = false;
        }
        //retire physics of cells
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            if(hasPhysics[x][physicsRadius * 2]){
                cells[x + drawRadius - physicsRadius][physicsRadius * 2 + drawRadius - physicsRadius].destroyPhysics();
            }
        }
        //shift physics array
        for(int x = physicsRadius * 2; x > 0; x--){
            for(int y = 0; y < physicsRadius * 2 + 1; y++){
                needsPhysics[x][y] = needsPhysics[x-1][y];
                hasPhysics[x][y] = hasPhysics[x-1][y];
            }
        }
        //invalidate edge of physics array
        for(int y = 0; y < physicsRadius * 2 + 1; y++){
            needsPhysics[0][y] = true;
            hasPhysics[0][y] = false;
        }
    }
    
    public void shiftChunksPosX(){
        //retire old graphics
        for(int y = 0; y < drawRadius * 2 + 1; y++){
            if(cells[0][y] != null){
                cells[0][y].retireCell();
            }
        }
        //shift draw array
        for(int x = 0; x < drawRadius * 2; x++){
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                cells[x][y] = cells[x+1][y];
                updateable[x][y] = true;
                hasRequested[x][y] = hasRequested[x+1][y];
            }
        }
        //invalidate edge of draw array
        for(int y = 0; y < drawRadius * 2 + 1; y++){
            valid[drawRadius * 2][y] = false;
            hasRequested[drawRadius * 2][y] = false;
        }
        //retire physics of cells
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            if(hasPhysics[x][physicsRadius * 2]){
                cells[x + drawRadius - physicsRadius][physicsRadius * 2 + drawRadius - physicsRadius].destroyPhysics();
            }
        }
        //shift physics array
        for(int x = 0; x < physicsRadius * 2; x++){
            for(int y = 0; y < physicsRadius * 2 + 1; y++){
                needsPhysics[x][y] = needsPhysics[x+1][y];
                hasPhysics[x][y] = hasPhysics[x+1][y];
            }
        }
        //invalidate edge of physics array
        for(int y = 0; y < physicsRadius * 2 + 1; y++){
            needsPhysics[physicsRadius * 2][y] = true;
            hasPhysics[physicsRadius * 2][y] = false;
        }
    }
    
    public void shiftChunksNegY(){
        //retire cells
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            if(cells[x][drawRadius * 2] != null){
                cells[x][drawRadius * 2].retireCell();
            }
        }
        //shift draw array
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            for(int y = drawRadius * 2; y > 0; y--){
                cells[x][y] = cells[x][y-1];
                updateable[x][y] = true;
                hasRequested[x][y] = hasRequested[x][y-1];
            }
        }
        //invalidate edge of draw array
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            valid[x][0] = false;
            hasRequested[x][0] = false;
        }
        //retire physics of cells
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            if(hasPhysics[x][physicsRadius * 2]){
                cells[x + drawRadius - physicsRadius][physicsRadius * 2 + drawRadius - physicsRadius].destroyPhysics();
            }
        }
        //shift physics array
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            for(int y = physicsRadius * 2; y > 0; y--){
                needsPhysics[x][y] = needsPhysics[x][y-1];
                hasPhysics[x][y] = hasPhysics[x][y-1];
            }
        }
        //invalidate edge of physics array
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            needsPhysics[x][0] = true;
            hasPhysics[x][0] = false;
        }
    }
    
    public void shiftChunksPosY(){
        //retire old graphics
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            if(cells[x][0] != null){
                cells[x][0].retireCell();
            }
        }
        //shift draw array
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            for(int y = 0; y < drawRadius * 2; y++){
                cells[x][y] = cells[x][y+1];
                updateable[x][y] = true;
                hasRequested[x][y] = hasRequested[x][y+1];
            }
        }
        //invalidate edge of draw array
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            valid[x][drawRadius * 2] = false;
            hasRequested[x][drawRadius * 2] = false;
        }
        //retire physics of cells
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            if(hasPhysics[x][0]){
                cells[x + drawRadius - physicsRadius][0 + drawRadius - physicsRadius].destroyPhysics();
            }
        }
        //shift physics array
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            for(int y = 0; y < physicsRadius * 2; y++){
                needsPhysics[x][y] = needsPhysics[x][y+1];
                hasPhysics[x][y] = hasPhysics[x][y+1];
            }
        }
        //invalidate edge of physics array
        for(int x = 0; x < physicsRadius * 2 + 1; x++){
            needsPhysics[x][physicsRadius * 2] = true;
            hasPhysics[x][physicsRadius * 2] = false;
        }
    }
    
    
    public int transformRealSpaceToCellSpace(double input){
        return (int)(input / clientWorldData.getDynamicInterpolationRatio());
    }
    
    
    public void invalidateAllCells(){
        for(int x = 0; x < drawRadius * 2 + 1; x++){
            for(int y = 0; y < drawRadius * 2 + 1; y++){
                valid[x][y] = false;
            }
        }
    }
    
    public void calculateDeltas(Vector3d oldPosition, Vector3d newPosition){
//        if(DRAW_CELL_MANAGER_FLAG_UPDATE_DYNAMIC_TERRAIN){
            if(transformRealSpaceToCellSpace(newPosition.x()) < transformRealSpaceToCellSpace(oldPosition.x())){
                shiftChunksNegX();
                setCellX(transformRealSpaceToCellSpace(newPosition.x()));
                setCellY(transformRealSpaceToCellSpace(newPosition.z()));
            } else if(transformRealSpaceToCellSpace(newPosition.x()) > transformRealSpaceToCellSpace(oldPosition.x())){
                shiftChunksPosX();
                setCellX(transformRealSpaceToCellSpace(newPosition.x()));
                setCellY(transformRealSpaceToCellSpace(newPosition.z()));
            }
            
            if(transformRealSpaceToCellSpace(newPosition.z()) < transformRealSpaceToCellSpace(oldPosition.z())){
                shiftChunksNegY();
                setCellX(transformRealSpaceToCellSpace(newPosition.x()));
                setCellY(transformRealSpaceToCellSpace(newPosition.z()));
            } else if(transformRealSpaceToCellSpace(newPosition.z()) > transformRealSpaceToCellSpace(oldPosition.z())){
                shiftChunksPosY();
                setCellX(transformRealSpaceToCellSpace(newPosition.x()));
                setCellY(transformRealSpaceToCellSpace(newPosition.z()));
            }
//        }
    }
    
    public void update(){
        if(update){
//        if(DRAW_CELL_MANAGER_FLAG_UPDATE_DYNAMIC_TERRAIN){
            if(containsInvalidCell()){
                updateInvalidCell();
            } else if(containsUndrawableCell()){
                makeCellDrawable();
            } else if(containsUpdateableCell()){
                updateCellModel();
            } else if(containsPhysicsNeedingCell()){
                addPhysicsToCell();
            }
        }
//        } else if(DRAW_CELL_MANAGER_FLAG_GENERATE_ARENA){
//            if(cells[0][0] == null){
//                int arenaChunkWidth = 100;
//                cells[0][0] = new DrawCell(
//                        new float[arenaChunkWidth + 1][arenaChunkWidth + 1],
//                        arenaChunkWidth + 1,
//                        0,
//                        0,
//                        arenaChunkWidth,
//                        program
//                );
//                cells[0][0].generateDrawableEntity(1);
//            } else if(cells[0][1] == null){
//                int arenaChunkWidth = 100;
//                cells[0][1] = new DrawCell(
//                        new float[arenaChunkWidth + 1][arenaChunkWidth + 1],
//                        arenaChunkWidth + 1,
//                        0,
//                        1,
//                        arenaChunkWidth,
//                        program
//                );
//                cells[0][1].generateDrawableEntity(1);
//            } else if(cells[1][0] == null){
//                int arenaChunkWidth = 100;
//                cells[1][0] = new DrawCell(
//                        new float[arenaChunkWidth + 1][arenaChunkWidth + 1],
//                        arenaChunkWidth + 1,
//                        1,
//                        0,
//                        arenaChunkWidth,
//                        program
//                );
//                cells[1][0].generateDrawableEntity(1);
//            } else if(cells[1][1] == null){
//                int arenaChunkWidth = 100;
//                cells[1][1] = new DrawCell(
//                        new float[arenaChunkWidth + 1][arenaChunkWidth + 1],
//                        arenaChunkWidth + 1,
//                        1,
//                        1,
//                        arenaChunkWidth,
//                        program
//                );
//                cells[1][1].generateDrawableEntity(1);
//            }
//        }
    }
    
//    public float getElevationAtRealPoint(float realPointX, float realPointY){
////        if(DRAW_CELL_MANAGER_FLAG_UPDATE_DYNAMIC_TERRAIN){
//            return terrainManager.getHeightAtPosition(realPointX, realPointY);
////        }
////        if(DRAW_CELL_MANAGER_FLAG_GENERATE_ARENA){
////            return 0;
////        }
////        return 0;
//    }
    
    
    public boolean canValidateCell(){
        boolean rVal = false;
        return rVal;
    }
    
    public boolean coordsInPhysicsSpace(int worldX, int worldY){
        return worldX <= cellX + physicsRadius && worldX >= cellX - physicsRadius && worldY <= cellY + physicsRadius && worldY >= cellY - physicsRadius;
    }
    
//    public 
    
}
