package electrosphere.engine;

import static org.lwjgl.glfw.GLFW.glfwTerminate;
import static org.lwjgl.glfw.GLFW.glfwWindowShouldClose;

import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;

import org.graalvm.polyglot.HostAccess.Export;

import org.ode4j.ode.OdeHelper;

import electrosphere.audio.AudioEngine;
import electrosphere.audio.VirtualAudioSourceManager;
import electrosphere.controls.ControlHandler;
import electrosphere.controls.ControlHandler.ControlsState;
import electrosphere.engine.cli.CLIParser;
import electrosphere.engine.loadingthreads.LoadingThread;
import electrosphere.engine.loadingthreads.LoadingThread.LoadingThreadType;
import electrosphere.engine.time.Timekeeper;
import electrosphere.game.config.UserSettings;
import electrosphere.game.server.world.MacroData;
import electrosphere.logger.LoggerInterface;
import electrosphere.menu.debug.ImGuiWindowMacros;
import electrosphere.renderer.RenderingEngine;
import electrosphere.server.MainServerFunctions;
import electrosphere.server.simulation.MacroSimulation;


/**
 * The main class
 */
public class Main {
    
    
    

    
    
    
    

    public static boolean running = true;
    
    //target amount of time per frame
    public static float targetFrameRate = 60.0f;
    static float targetFramePeriod = 1.0f/targetFrameRate;

    //framestep variable
    static int framestep = 2;
    
    public static void main(String args[]){
        
        //
        //
        //     I N I T I A L I Z A T I O N
        //
        //

        //parse command line arguments
        CLIParser.parseCLIArgs(args);

        startUp();
        
        mainLoop();

    }

    public static void startUp(){
        //initialize logging interfaces
        LoggerInterface.initLoggers();

        //gets java pid of engine
        Globals.javaPID = ManagementFactory.getRuntimeMXBean().getName();

        //load user settings
        UserSettings.loadUserSettings();
        
        //init global variables
        Globals.initGlobals();

        //init scripting engine
        Globals.scriptEngine.init();

        //controls
        if(Globals.RUN_CLIENT){
            initControlHandler();
        }

        //init ODE
        OdeHelper.initODE();

        // if(1==1){
        //     SaveUtils.loadSave("arena");
        //     Globals.authenticationManager = new AuthenticationManager();
        //     String rawPass = "testpassword";
        //     String hashedPassword = AuthenticationManager.getHashedString(rawPass);
        //     boolean authed = Globals.authenticationManager.authenticate("testuser", hashedPassword);
        //     System.out.println("Authenticated: " + authed);
        //     System.exit(0);
        // }
        
        //world gen testing
        //gen terrain
        if(Globals.RUN_SIMULATION_ONLY){
            //gen world
            Globals.macroData = MacroData.generateWorld(0);
            Globals.macroData.describeWorld();
            boolean run = true;
            Globals.macroSimulation = new MacroSimulation();
            while(run){
                Globals.macroSimulation.simulate();
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }

//        if(1==1){
//            Globals.audioEngine = new AudioEngine();
//            Globals.audioEngine.init();
//            Globals.assetManager.addAudioPathToQueue("/Audio/MenuStartup.ogg");
//            Globals.assetManager.loadAssetsInQueue();
//            Globals.audioEngine.setGain(0.1f);
//            AudioSource startupSound = AudioUtils.playAudioAtLocation("/Audio/MenuStartup.ogg", new Vector3f(3,0,0));
//            if(startupSound != null){
//                while(startupSound.isPlaying()){
//                    try {
//                        TimeUnit.MILLISECONDS.sleep(10);
//                    } catch (InterruptedException ex) {
//                        ex.printStackTrace();
//                    }
//                }
//            }
//            System.exit(0);
//        }

        

        //debug: create terrain/world viewer
    //    TerrainViewer.runViewer();
        
        //create the drawing context
        if(Globals.RUN_CLIENT && !Globals.HEADLESS){
            //create opengl context
            Globals.renderingEngine = new RenderingEngine();
            Globals.renderingEngine.createOpenglContext();
            Globals.initDefaultGraphicalResources();
            ImGuiWindowMacros.initImGuiWindows();

            //inits the controls state of the control handler
            Globals.controlHandler.hintUpdateControlState(ControlsState.TITLE_MENU);

            //start initial asset loading
            new Thread(Globals.initialAssetLoadingThread).start();
        }

        //Sets a hook that fires when the engine process stops
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            LoggerInterface.loggerEngine.INFO("Shutdown hook!");
        }));
        
        //uncomment to test loading a model into engine
        // if(1==1){
        //     //Models/creatures/person2/person2_1.glb
        //     ///Models/creatures/viewmodel.glb
        //     Globals.assetManager.addModelPathToQueue("/Models/creatures/person2/person2_1.glb");
        //     Globals.assetManager.loadAssetsInQueue();
        //     electrosphere.renderer.model.Model model = Globals.assetManager.fetchModel("/Models/creatures/person2/person2_1.glb");
        //     //    for(electrosphere.renderer.anim.Animation anim : model.getAnimations()){
        //     //        if(anim.name.equals("Armature|Idle1")){
        //     //            System.out.println(anim.duration);
        //     //            for(electrosphere.renderer.anim.AnimChannel channel : anim.channels){
        //     //                if(channel.getNodeID().equals("Torso")){
        //     //                    channel.fullDescribeChannel();
        //     //                }
        //     //             //    System.out.println("CHannel: " + channel.getNodeID());
        //     //            }
        //     //            break;
        //     //        }
        //     //    }
        //         model.describeHighLevel();
        //         // model.animations.get(0).fullDescribeAnimation();
        //     //    model.describeHighLevel();
        //     System.exit(0);
        // }
        
        //create the audio context
        Globals.audioEngine = new AudioEngine();
        if(Globals.RUN_CLIENT && !Globals.HEADLESS && Globals.RUN_AUDIO){
            Globals.virtualAudioSourceManager = new VirtualAudioSourceManager();
            Globals.audioEngine.init();
            Globals.audioEngine.listAllDevices();
            Globals.initDefaultAudioResources();
            // Globals.audioEngine.setGain(0.1f);
        }

        //init timekeeper
        Globals.timekeeper.init(targetFramePeriod);
        
        //fire off a loading thread for the title menus/screen
        LoggerInterface.loggerStartup.INFO("Fire off loading thread");
        if(Globals.RUN_DEMO){
            LoadingThread serverThread = new LoadingThread(LoadingThreadType.DEMO_MENU);
            Globals.loadingThreadsList.add(serverThread);
            serverThread.start();
        } else if(Globals.RUN_CLIENT){
            LoadingThread serverThread = new LoadingThread(LoadingThreadType.TITLE_MENU);
            Globals.loadingThreadsList.add(serverThread);
            serverThread.start();
        } else {
            throw new IllegalStateException("Need to add handling for only running server again");
        }
        
        //recapture the screen for rendering
        if(Globals.RUN_CLIENT && !Globals.HEADLESS){
            LoggerInterface.loggerStartup.INFO("Recapture screen");
            Globals.controlHandler.setRecapture(true);
        }
        // RenderUtils.recaptureScreen();
    }

    /**
     * Runs the main loop indefinitely. Blocks the thread this is called in.
     */
    public static void mainLoop(){
        mainLoop(0);
        shutdown();
    }

    /**
     * Runs the main loop for a specified number of frames.
     * @param maxFrames The number of frames to run for. If 0, will run indefinitely.
     */
    public static void mainLoop(long maxFrames){

        //resets running flag to that we can repeatedly loop (ie in tests)
        running = true;

        //main loop
        while (running) {
            try {

            Globals.profiler.beginRootCpuSample("frame");
            LoggerInterface.loggerEngine.DEBUG_LOOP("Begin Main Loop Frame");

            //
            //Update timekeeper
            //
            Globals.timekeeper.update();

            
            
            ///
            ///    A S S E T     M A N A G E R     S T U F F
            ///
            if(Globals.RUN_CLIENT){
                Globals.profiler.beginCpuSample("Load Assets");
                LoggerInterface.loggerEngine.DEBUG_LOOP("Begin load assets");
                Globals.assetManager.loadAssetsInQueue();
                Globals.profiler.endCpuSample();
            }
            
            
            
            
            ///
            ///    C L I E N T    N E T W O R K I N G    S T U F F
            ///
            //Why is this its own function? Just to get the networking code out of main()
            if(Globals.clientConnection != null){
                Globals.profiler.beginCpuSample("Client networking");
                LoggerInterface.loggerEngine.DEBUG_LOOP("Begin parse client messages");
                Globals.clientConnection.parseMessagesSynchronous();
                Globals.profiler.endCpuSample();
            }

            
            
            ///
            ///    I N P U T     C O N T R O L S
            ///
            //Poll controls
            if(Globals.RUN_CLIENT){
                Globals.profiler.beginCpuSample("Poll Controls");
                LoggerInterface.loggerEngine.DEBUG_LOOP("Begin recapture screen");
                Globals.controlHandler.pollControls();
                RenderingEngine.recaptureIfNecessary();
                Globals.profiler.endCpuSample();
            }
            
            
            ///
            ///
            ///    M A I N     S I M U L A T I O N     I N N E R     L O O P
            ///

            int simFrameHardcapCounter = 0;
            while(Globals.timekeeper.pullFromAccumulator() && framestep > 0 && simFrameHardcapCounter < Timekeeper.SIM_FRAME_HARDCAP){

                //sim frame hard cap counter increment
                simFrameHardcapCounter++;
                //handle framestep
                if(framestep == 1){
                    framestep = 0;
                }



                ///
                ///    C L I E N T    S I M U L A T I O N    S T U F F
                ///
                LoggerInterface.loggerEngine.DEBUG_LOOP("Begin client simulation");
                if(Globals.clientSimulation != null){
                    Globals.profiler.beginCpuSample("Client simulation");
                    Globals.clientSimulation.simulate();
                    Globals.profiler.endCpuSample();
                }









                

                ///
                ///    S E R V E R     M A I N     R O U T I N E S
                ///
                Globals.profiler.beginCpuSample("Main Server Functions");
                MainServerFunctions.simulate();
                Globals.profiler.endCpuSample();
            }
            



            ///
            ///    M A I N   R E N D E R   F U N C T I O N
            ///
            LoggerInterface.loggerEngine.DEBUG_LOOP("Begin rendering call");
            if(Globals.RUN_CLIENT && !Globals.HEADLESS){
                Globals.profiler.beginCpuSample("render");
                Globals.renderingEngine.drawScreen();
                Globals.profiler.endCpuSample();
            }




            ///
            ///     G A R B A G E    C H E C K
            ///
            Globals.profiler.beginCpuSample("gc");
            if(Globals.EXPLICIT_GC){
                System.gc();
            }
            Globals.profiler.endCpuSample();





            ///
            ///   S H U T D O W N    C H E C K
            ///
            if(Globals.HEADLESS){
                if(Globals.ENGINE_SHUTDOWN_FLAG){
                    running = false;
                }
            } else {
                if(Globals.ENGINE_SHUTDOWN_FLAG || (Globals.RUN_CLIENT && glfwWindowShouldClose(Globals.window))){
                    running = false;
                }
            }
            





            ///
            ///     C L E A N U P    T I M E    V A R I A B L E S
            ///
            Globals.profiler.beginCpuSample("sleep");
            if(Globals.timekeeper.getMostRecentRawFrametime() < targetFramePeriod){
                sleep((int)(1000.0 * (targetFramePeriod - Globals.timekeeper.getMostRecentRawFrametime())));
            } else {
                sleep(1);
            }
            Globals.profiler.endCpuSample();
            Globals.timekeeper.numberOfRenderedFrames++;
            if(maxFrames > 0 && Globals.timekeeper.numberOfRenderedFrames > maxFrames){
                running = false;
            }


            ///
            ///     E N D     M A I N     L O O P
            ///
            LoggerInterface.loggerEngine.DEBUG_LOOP("End Main Loop Frame");
            Globals.profiler.endCpuSample();

            } catch (NullPointerException ex){
                LoggerInterface.loggerEngine.ERROR("Main frame uncaught NPE", ex);
                //after a while, jvm will stop reporting stack traces with errors
                //need to explicitly kill the vm if you want to see the stack trace
                if(Globals.ENGINE_DEBUG){
                    System.exit(1);
                }
            }

        }

    }

    /**
     * Shuts down the engine
     */
    public static void shutdown(){
        LoggerInterface.loggerEngine.WARNING("ENGINE SHUTDOWN");
        //
        //   S H U T D O W N
        //
        //Terminate the program.
        if(!Globals.HEADLESS && Globals.RUN_CLIENT){
            glfwTerminate();
        }
        //used to signal threads to stop
        running = false;
        if(Globals.server != null){
            Globals.server.close();
            if(Globals.serverThread != null){
                Globals.serverThread.interrupt();
            }
        }
        //shut down audio engine
        if(!Globals.HEADLESS && Globals.RUN_CLIENT && Globals.RUN_AUDIO && Globals.audioEngine != null && Globals.audioEngine.initialized()){
            Globals.audioEngine.shutdown();
        }
        //if netmonitor is running, close
        if(Globals.netMonitor != null){
            Globals.netMonitor.close();
        }
        //shutdown ode
        OdeHelper.closeODE();
    }

    static void sleep(int i) {
        try {
            TimeUnit.MILLISECONDS.sleep(i);
        } catch (InterruptedException ex) {
            System.out.println("Sleep somehow interrupted?!");
        }
    }
    
    
    public static boolean isRunning(){
        return running;
    }
    
    
    
    
    public static void initControlHandler(){
        LoggerInterface.loggerStartup.INFO("Initialize control handler");
        Globals.controlHandler = ControlHandler.generateExampleControlsMap();
        Globals.controlHandler.setCallbacks();
//        Globals.controlHandler = FileLoadingUtils.loadModelObjectFromBakedJsonFile("/Config/keybinds.json",ControlHandler.class);
    }
    
    /**
     * Sets the framestep state (2 to resume automatic, 1 to make single step)
     * @param framestep 2 - automatic framestep, 1 - single step, 0 - no step
     */
    @Export
    public static void setFramestep(int framestep){
        Main.framestep = framestep;
    }

    
    
    
    // public static void updateMouseVariables(){
    //     glfwGetCursorPos(Globals.window, mouse_X_Buffer, mouse_Y_Buffer);
    //     xpos = mouse_X_Buffer[0];
    //     ypos = mouse_Y_Buffer[0];
    //     float xoffset = (float) (xpos - mouse_lastX) * mouseSensitivity;
    //     float yoffset = (float) (mouse_lastY - ypos) * mouseSensitivity;
    //     mouse_lastX = (float) xpos;
    //     mouse_lastY = (float) ypos;

    //     if(Globals.controlHandler != null && !Globals.controlHandler.isMouseVisible()){
    //         yaw = yaw + xoffset;
    //         pitch = pitch - yoffset;

    //         if (pitch > 100.0f) {
    //             pitch = 100.0f;
    //         }
    //         if (pitch < -99.0f) {
    //             pitch = -99.0f;
    //         }
    //     }
    // }
    
    
    
}
