package electrosphere.engine.loadingthreads;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import org.joml.Vector3d;
import org.joml.Vector3f;

import electrosphere.client.block.cells.ClientBlockCellManager;
import electrosphere.client.entity.camera.CameraEntityUtils;
import electrosphere.client.entity.crosshair.Crosshair;
import electrosphere.client.fluid.cells.FluidCellManager;
import electrosphere.client.sim.ClientSimulation;
import electrosphere.client.terrain.cells.ClientDrawCellManager;
import electrosphere.client.terrain.foliage.FoliageCellManager;
import electrosphere.client.ui.menu.MenuGenerators;
import electrosphere.client.ui.menu.WindowStrings;
import electrosphere.client.ui.menu.WindowUtils;
import electrosphere.client.ui.menu.mainmenu.MenuCharacterCreation;
import electrosphere.controls.ControlHandler;
import electrosphere.engine.Globals;
import electrosphere.engine.assetmanager.AssetDataStrings;
import electrosphere.engine.signal.Signal.SignalType;
import electrosphere.engine.threads.LabeledThread.ThreadLabel;
import electrosphere.entity.DrawableUtils;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityCreationUtils;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.logger.LoggerInterface;
import electrosphere.net.NetUtils;
import electrosphere.net.client.ClientNetworking;
import electrosphere.net.parser.net.message.CharacterMessage;
import electrosphere.net.parser.net.message.LoreMessage;
import electrosphere.renderer.actor.Actor;
import electrosphere.renderer.actor.ActorTextureMask;

public class ClientLoading {
    

    /**
     * Number of frames to wait before updating status of draw cell manager loading
     */
    static final int DRAW_CELL_UPDATE_RATE = 60;


    /**
     * The number of frames the draw cell is expected to take (minimum) to init
     */
    static final int DRAW_CELL_EXPECTED_MINIMUM_FRAMES_TO_INIT = 10;

    /**
     * Number of frames to wait before re-sending query for lore
     */
    static final int LORE_RESEND_FRAMES = 1000;


    /**
     * Loads the race data from the server
     * @param params no params
     */
    protected static void loadCharacterServer(Object[] params){
        if(params.length < 1){
            throw new Error("Expected 1 params!");
        }

        boolean useLocalConnection = (boolean)params[0];

        WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_MENU_MAIN), false);
        WindowUtils.replaceMainMenuContents(MenuGenerators.createEmptyMainMenu());
        WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_LOADING), true);
        WindowUtils.updateLoadingWindow("Waiting on server");
        //disable menu input
        Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.NO_INPUT);
        //initialize the client thread (client)
        if(useLocalConnection){
            LoadingUtils.initLocalConnection(true);
        } else {
            ClientLoading.initClientThread();
        }
        //while we don't know what races are playable, wait
        WindowUtils.updateLoadingWindow("Waiting on lore");
        int framesWaited = 0;
        while(Globals.gameConfigCurrent.getCreatureTypeLoader().getPlayableRaces().size() == 0){
            if(framesWaited % LORE_RESEND_FRAMES == (LORE_RESEND_FRAMES - 1)){
                //request playable races
                Globals.clientConnection.queueOutgoingMessage(LoreMessage.constructRequestRacesMessage());
                //request characters available to this player
                Globals.clientConnection.queueOutgoingMessage(CharacterMessage.constructRequestCharacterListMessage());
            }
            try {
                TimeUnit.MILLISECONDS.sleep(5);
            } catch (InterruptedException ex) {}
            framesWaited++;
        }
        //once we have them, bring up the character creation interface
        //init character creation window
        //eventually should replace with at ui to select an already created character or create a new one
        WindowUtils.replaceMainMenuContents(MenuCharacterCreation.createCharacterSelectionWindow());
        //make loading dialog disappear
        WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_LOADING), false);
        //make character creation window visible
        WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_MENU_MAIN), true);
        //recapture window
        Globals.controlHandler.setRecapture(true);
        //log
        LoggerInterface.loggerEngine.INFO("[Client]Finished loading character creation menu");
        //set menu controls again
        Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.TITLE_MENU);
    }


    protected static void loadClientWorld(Object[] params){
        Globals.signalSystem.post(SignalType.UI_MODIFICATION, () -> {
            WindowUtils.closeWindow(WindowStrings.WINDOW_MENU_MAIN);
            WindowUtils.recursiveSetVisible(WindowStrings.WINDOW_LOADING, true);
            WindowUtils.updateLoadingWindow("LOADING");
        });
        //disable menu input
        Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.NO_INPUT);
        //initialize the "real" objects simulation
        initClientSimulation();
        LoadingUtils.setSimulationsToReady();
        //initialize the gridded managers (client)
        initDrawCellManager(true);
        initFoliageManager();
        initFluidCellManager(true);
        initBlockCellManager(true);
        //initialize the basic graphical entities of the world (skybox, camera)
        initWorldBaseGraphicalEntities();
        //init arena specific stuff (ie different skybox colors)
        initArenaGraphicalEntities();
        //sets micro and macro sims to ready if they exist
        setSimulationsToReady();
        //set simulations to ready if they exist
        LoadingUtils.setSimulationsToReady();
        //make loading window disappear
        Globals.signalSystem.post(SignalType.UI_MODIFICATION, () -> {
            WindowUtils.recursiveSetVisible(WindowStrings.WINDOW_LOADING, false);
            Globals.RENDER_FLAG_RENDER_SHADOW_MAP = true;
            Globals.RENDER_FLAG_RENDER_SCREEN_FRAMEBUFFER_CONTENT = true;
            Globals.RENDER_FLAG_RENDER_SCREEN_FRAMEBUFFER = true;
            Globals.RENDER_FLAG_RENDER_UI = true;
            Globals.RENDER_FLAG_RENDER_BLACK_BACKGROUND = false;
            Globals.RENDER_FLAG_RENDER_WHITE_BACKGROUND = false;
        });
        //recapture screen
        Globals.controlHandler.setRecapture(true);
        LoggerInterface.loggerEngine.INFO("[Client]Finished loading main game");
        //set controls state
        Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.MAIN_GAME);
    }

    /**
     * Loads the viewport
     */
    protected static void loadViewport(Object[] params){
        Globals.signalSystem.post(SignalType.UI_MODIFICATION, () -> {
            WindowUtils.closeWindow(WindowStrings.WINDOW_MENU_MAIN);
            WindowUtils.recursiveSetVisible(WindowStrings.WINDOW_LOADING, true);
        });
        //disable menu input
        Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.NO_INPUT);

        //init camera
        Globals.playerCamera = CameraEntityUtils.spawnBasicCameraEntity(new Vector3d(0,0,0), new Vector3d(-1,0,0));
        Globals.cameraHandler.setTrackPlayerEntity(false);
        Globals.cameraHandler.setUpdate(false);
        //initialize the "real" objects simulation
        initClientSimulation();
        //initialize the cell managers (client)
        initDrawCellManager(false);
        initFluidCellManager(false);
        initBlockCellManager(false);
        initFoliageManager();

        //sets micro and macro sims to ready if they exist
        setSimulationsToReady();
        //make loading window disappear
        Globals.signalSystem.post(SignalType.UI_MODIFICATION, () -> {
            WindowUtils.recursiveSetVisible(WindowStrings.WINDOW_LOADING, false);
            Globals.RENDER_FLAG_RENDER_SHADOW_MAP = true;
            Globals.RENDER_FLAG_RENDER_SCREEN_FRAMEBUFFER_CONTENT = true;
            Globals.RENDER_FLAG_RENDER_SCREEN_FRAMEBUFFER = true;
            Globals.RENDER_FLAG_RENDER_UI = true;
            Globals.RENDER_FLAG_RENDER_BLACK_BACKGROUND = false;
            Globals.RENDER_FLAG_RENDER_WHITE_BACKGROUND = false;
        });
        //recapture screen
        Globals.controlHandler.setRecapture(true);
        LoggerInterface.loggerEngine.INFO("[Client]Finished loading main game");
        //set controls state
        Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.MAIN_GAME);
    }










    /**
     * Inits the client networking thread and socket
     */
    private static void initClientThread(){
        //start client networking
        if(Globals.RUN_CLIENT){
            Globals.clientConnection = new ClientNetworking(NetUtils.getAddress(),NetUtils.getPort());
            Globals.threadManager.start(ThreadLabel.NETWORKING_CLIENT, new Thread(Globals.clientConnection));
        }
    }

    /**
     * Creates client simulation object
     */
    private static void initClientSimulation(){
        if(Globals.clientSimulation == null){
            Globals.clientSimulation = new ClientSimulation();
        }
    }

    /**
     * Sets client simulation object state to ready
     */
    private static void setSimulationsToReady(){
        Globals.clientSimulation.setReady(true);
    }


    private static void initWorldBaseGraphicalEntities(){
        /*
        
        Skybox
        
        */
        // Model skyboxModel = Globals.assetManager.fetchModel(AssetDataStrings.ASSET_STRING_SKYBOX_BASIC);
        // Globals.skybox = EntityUtils.spawnDrawableEntity(AssetDataStrings.ASSET_STRING_SKYBOX_BASIC);
        
        
        /*
        
        Player Camera
        
        */
        CameraEntityUtils.initCamera();

        
        /*
        Targeting crosshair
        */
        Crosshair.initCrossHairEntity();
        
    }


    static void initArenaGraphicalEntities(){

        float skyR = 150;
        float skyG = 200;
        float skyB = 250;

        float groundR = 20;
        float groundG = 20;
        float groundB = 20;

        Globals.skyboxColors.add(new Vector3f(skyR,skyG,skyB));
        Globals.skyboxColors.add(new Vector3f(skyR,skyG,skyB));
        Globals.skyboxColors.add(new Vector3f(groundR,groundG,groundB));
        Globals.skyboxColors.add(new Vector3f(groundR,groundG,groundB));
        Globals.skyboxColors.add(new Vector3f(skyR,skyG,skyB));
        Globals.skyboxColors.add(new Vector3f(skyR,skyG,skyB));
        Globals.skyboxColors.add(new Vector3f(groundR,groundG,groundB));
        Globals.skyboxColors.add(new Vector3f(groundR,groundG,groundB));
        
        //starry sky true skybox
        Entity skybox = EntityCreationUtils.createClientSpatialEntity();
        EntityCreationUtils.makeEntityDrawable(skybox, "Models/environment/skyboxSphere.fbx");
        DrawableUtils.disableCulling(skybox);
        EntityUtils.getRotation(skybox).rotateX((float)(-Math.PI/2.0f));
        EntityUtils.getScale(skybox).mul(600000.0f);
        Globals.clientScene.registerBehaviorTree(() -> {
            EntityUtils.getPosition(skybox).set(EntityUtils.getPosition(Globals.playerEntity));
        });
        Globals.assetManager.queueOverrideMeshShader("Models/environment/skyboxSphere.fbx", "Sphere", "Shaders/entities/skysphere/skysphere.vs", "Shaders/entities/skysphere/skysphere.fs");

        //cloud ring pseudo skybox
        // Entity cloudRing = EntityCreationUtils.createClientSpatialEntity();
        // EntityCreationUtils.makeEntityDrawable(cloudRing, "Models/environment/cloudRing.fbx");
        // DrawableUtils.makeEntityTransparent(cloudRing);
        // DrawableUtils.disableCulling(cloudRing);
        // EntityUtils.getRotation(cloudRing).rotateX((float)(-Math.PI/2.0f));
        // EntityUtils.getScale(cloudRing).mul(100000.0f);
        // Globals.clientScene.registerBehaviorTree(new ApplyRotationTree(cloudRing,new Quaterniond().rotationZ(0.0001)));
        // Globals.assetManager.queueOverrideMeshShader("Models/environment/cloudRing.fbx", "Sphere", "Shaders/skysphere/skysphere.vs", "Shaders/skysphere/skysphere.fs");

        //player's cursor
        Globals.playerCursor = EntityCreationUtils.createClientSpatialEntity();
        EntityCreationUtils.makeEntityDrawable(Globals.playerCursor, AssetDataStrings.UNITSPHERE);
        Actor cursorActor = EntityUtils.getActor(Globals.playerCursor);
        cursorActor.addTextureMask(new ActorTextureMask("sphere", Arrays.asList(new String[]{"Textures/transparent_red.png"})));
        DrawableUtils.makeEntityTransparent(Globals.playerCursor);
        EntityUtils.getScale(Globals.playerCursor).set(0.2f);

        //player's block cursor
        Globals.playerBlockCursor = EntityCreationUtils.createClientSpatialEntity();
        EntityCreationUtils.makeEntityDrawable(Globals.playerBlockCursor, AssetDataStrings.UNITCUBE);
        Actor blockCursorActor = EntityUtils.getActor(Globals.playerBlockCursor);
        blockCursorActor.addTextureMask(new ActorTextureMask("cube", Arrays.asList(new String[]{"Textures/transparent_red.png"})));
        DrawableUtils.makeEntityTransparent(Globals.playerBlockCursor);
        EntityUtils.getScale(Globals.playerBlockCursor).set(0.2f);
        Globals.clientSceneWrapper.getScene().removeEntityFromTag(Globals.playerBlockCursor, EntityTags.DRAWABLE);

        //cloud object
        // Entity cloudEnt = EntityCreationUtils.createClientSpatialEntity();
        // EntityCreationUtils.makeEntityDrawablePreexistingModel(cloudEnt, Globals.assetManager.queuedAsset(new QueuedModel(() -> {
        //     Model rVal = new Model();

        //     Mesh m = GeometryMeshGen.genBox(1, 1, 1);

        //     //shader logic
        //     m.setShader(VisualShader.loadSpecificShader("Shaders/entities/clouds/clouds.vs", "Shaders/entities/clouds/clouds.fs"));
        //     m.setParent(rVal);
            
        //     rVal.getMeshes().add(m);
        //     rVal.setBoundingSphere(0,0,0,2);
        //     return rVal;
        // })));
    }

    static final int MAX_DRAW_CELL_WAIT = 1000;

    /**
     * Inits the drawcell manager
     * @param blockForInit Blocks the thread until the draw cell manager is ready
     */
    static void initDrawCellManager(boolean blockForInit){
        int iterations = 0;
        WindowUtils.updateLoadingWindow("WAITING ON WORLD DATA");
        while(blockForInit && (Globals.clientWorldData == null || InitialAssetLoading.atlasQueuedTexture == null || !InitialAssetLoading.atlasQueuedTexture.hasLoaded()) && Globals.threadManager.shouldKeepRunning()){
            try {
                TimeUnit.MILLISECONDS.sleep(10);
                iterations++;
            } catch (InterruptedException ex) {
                LoggerInterface.loggerEngine.ERROR(ex);
            }
            if(iterations > MAX_DRAW_CELL_WAIT){
                String message = "Draw cell took too long to init!\n" +
                Globals.clientWorldData + "\n" +
                InitialAssetLoading.atlasQueuedTexture.hasLoaded();
                throw new IllegalStateException(message);
            }
        }
        //initialize draw cell manager
        // Globals.drawCellManager = new DrawCellManager(Globals.clientTerrainManager, 0, 0, 0);
        Globals.clientDrawCellManager = new ClientDrawCellManager(Globals.voxelTextureAtlas, Globals.clientWorldData.getWorldDiscreteSize());
        //Alerts the client simulation that it should start loading terrain
        Globals.clientSimulation.setLoadingTerrain(true);
        //wait for all the terrain data to arrive
        int i = 0;
        while(
            blockForInit &&
            !Globals.clientDrawCellManager.isInitialized() &&
            Globals.threadManager.shouldKeepRunning()
        ){
            i++;
            if(i % DRAW_CELL_UPDATE_RATE == 0){
                WindowUtils.updateLoadingWindow("WAITING ON SERVER TO SEND TERRAIN (" + Globals.clientTerrainManager.getAllChunks().size() + ")");
            }
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        if(i < DRAW_CELL_EXPECTED_MINIMUM_FRAMES_TO_INIT){
            LoggerInterface.loggerEngine.WARNING("Draw cell manager loaded exceptionally fast!");
        }
    }

    /**
     * Inits the fluid cell manager
     * @param blockForInit Blocks the thread until the fluid cell manager is ready
     */
    static void initFluidCellManager(boolean blockForInit){

        //wait for world data
        WindowUtils.updateLoadingWindow("WAITING ON WORLD DATA");
        while(blockForInit && Globals.clientWorldData == null && Globals.threadManager.shouldKeepRunning()){
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException ex) {
            }
        }

        //initialize draw cell manager
        Globals.fluidCellManager = new FluidCellManager(Globals.clientTerrainManager, 0, 0, 0);
        Globals.fluidCellManager.setGenerateDrawables(true);
        Globals.clientSimulation.setLoadingTerrain(true);

        //wait for all the terrain data to arrive
        WindowUtils.updateLoadingWindow("REQUESTING FLUID CHUNKS FROM SERVER (" + Globals.fluidCellManager.getUnrequestedSize() + ")");
        while(blockForInit && Globals.fluidCellManager.containsUnrequestedCell() && Globals.threadManager.shouldKeepRunning() && Globals.RUN_FLUIDS){
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        
        //wait for undrawable cells
        // WindowUtils.updateLoadingWindow("WAITING ON SERVER TO SEND FLUID CHUNKS (" + Globals.fluidCellManager.getUndrawableSize() + ")");
        // while(blockForInit && Globals.fluidCellManager.containsUndrawableCell() && Globals.threadManager.shouldKeepRunning()){
        //     try {
        //         TimeUnit.MILLISECONDS.sleep(10);
        //     } catch (InterruptedException ex) {
        //         ex.printStackTrace();
        //     }
        // }
    }

    /**
     * Inits the block cell manager
     * @param blockForInit Blocks the thread until the block cell manager is ready
     */
    static void initBlockCellManager(boolean blockForInit){
        int iterations = 0;
        WindowUtils.updateLoadingWindow("WAITING ON WORLD DATA");
        while(blockForInit && (Globals.clientWorldData == null || InitialAssetLoading.atlasQueuedTexture == null || !InitialAssetLoading.atlasQueuedTexture.hasLoaded()) && Globals.threadManager.shouldKeepRunning()){
            try {
                TimeUnit.MILLISECONDS.sleep(10);
                iterations++;
            } catch (InterruptedException ex) {
                LoggerInterface.loggerEngine.ERROR(ex);
            }
            if(iterations > MAX_DRAW_CELL_WAIT){
                String message = "Draw cell took too long to init!\n" +
                Globals.clientWorldData + "\n" +
                InitialAssetLoading.atlasQueuedTexture.hasLoaded();
                throw new IllegalStateException(message);
            }
        }
        Globals.clientBlockCellManager = new ClientBlockCellManager(Globals.blockTextureAtlas, Globals.clientWorldData.getWorldDiscreteSize());
        //Alerts the client simulation that it should start loading blocks
        Globals.clientSimulation.setLoadingTerrain(true);
        //wait for all the block data to arrive
        int i = 0;
        while(
            blockForInit &&
            !Globals.clientBlockCellManager.isInitialized() &&
            Globals.threadManager.shouldKeepRunning()
        ){
            i++;
            if(i % DRAW_CELL_UPDATE_RATE == 0){
                WindowUtils.updateLoadingWindow("WAITING ON SERVER TO SEND BLOCKS (" + Globals.clientTerrainManager.getAllChunks().size() + ")");
            }
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        if(i < DRAW_CELL_EXPECTED_MINIMUM_FRAMES_TO_INIT){
            LoggerInterface.loggerEngine.WARNING("Block cell manager loaded exceptionally fast!");
        }
    }

    /**
     * Starts up the foliage manager
     */
    private static void initFoliageManager(){
        Globals.foliageCellManager = new FoliageCellManager(Globals.clientWorldData.getWorldDiscreteSize());
        Globals.foliageCellManager.init();
        // Globals.foliageCellManager.start();
    }




}
