package net.halbear.Terrain4J.EngineCore.Main;

import imgui.ImGui;
import imgui.ImGuiIO;
import imgui.ImGuiViewport;
import imgui.ImVec2;
import imgui.flag.ImGuiCond;
import imgui.flag.ImGuiWindowFlags;
import net.halbear.Terrain4J.EngineCore.Audio.AudioManager;
import net.halbear.Terrain4J.EngineCore.Audio.AudioSource;
import net.halbear.Terrain4J.EngineCore.Audio.SoundBuffer;
import net.halbear.Terrain4J.EngineCore.Audio.SoundListener;
import net.halbear.Terrain4J.EngineCore.Display.Window;
import net.halbear.Terrain4J.EngineCore.Input.KeyboardInput;
import net.halbear.Terrain4J.EngineCore.Input.MouseListener;
import net.halbear.Terrain4J.EngineCore.Logic.*;
import net.halbear.Terrain4J.EngineCore.Logic.Rendering.ModelLoader;
import net.halbear.Terrain4J.EngineCore.Main.Scene.*;
import net.halbear.Terrain4J.EngineCore.Profiling.CPUMonitor;
import net.halbear.Terrain4J.EngineCore.Profiling.GPUProfiler;
import net.halbear.Terrain4J.EngineCore.Vulkan.GUI.GuiTexture;
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.VkModel.MaterialData;
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.VkModel.ModelData;
import net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DisplayToScreen.SceneRender;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.lwjgl.openal.AL11;

import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;

import static org.lwjgl.glfw.GLFW.*;

public class GameCore implements GameLogic {

    private static final float MOUSE_SENSITIVITY = 0.1f;
    private static final float MOVEMENT_SPEED = 0.025f;

    private static final String SOUND_BUFFER_MUSIC = "music-sound-buffer";
    private static final String SOUND_BUFFER_PLAYER = "player-sound-buffer";
    private static final String SOUND_SOURCE_MUSIC = "music-sound-source";
    private static final String SOUND_SOURCE_PLAYER = "player-sound-source";

    private final Vector3f rotatingAngle = new Vector3f(0, 1, 0);
    private float angle = 180;
    private List<Float> angles = new ArrayList<>();
    private List<Vector3f> rotatingAngles = new ArrayList<>();
    private List<Vector3f> Velocities = new ArrayList<>();
    private Actor CubeActor;
    private List<Actor> CubeActors = new ArrayList<>();
    private List<JackOfAllTradesActor> entities = new ArrayList<>();
    private List<ParticleActor> particleActors = new ArrayList<>();
    private Vector2f LastMousePos = new Vector2f(0,0);
    private int Ticks = 0;
    private int GUI_MODE = 0;
    private GuiTexture guiTexture;
    private List<String[]> PerformanceMetrics = new ArrayList<>();
    private List<String[]> SettingPerformanceMetrics = new ArrayList<>();
    private boolean FetchingMetrics = false;
    private boolean ChangeGraphicsSettings = false;
    private int NextAAMode = -1;
    private long LastFrameTime = 0;
    private long CoolDown = 1000;
    private ModelData MusicParticle;
    private ModelData radio;

    @Override
    public void cleanup() {

    }

    @Override
    public InitData Initialise(EngineInstance engineInstance) {
        Scene scene = engineInstance.scene();
        List<ModelData> models = new ArrayList<>();
        MusicParticle = ModelLoader.LoadModel("resources/models/MusicParticle/MusicParticle.json");
        List<MaterialData> MusicParticleMat = ModelLoader.LoadMaterials("resources/models/MusicParticle/MusicParticle_mat.json");
        ModelData SponzaData = ModelLoader.LoadModel("resources/models/Forest/forest.json");
        List<MaterialData> SponzaMaterial = ModelLoader.LoadMaterials("resources/models/Forest/forest_mat.json");
        radio = ModelLoader.LoadModel("resources/models/radio/radio.json");
        List<MaterialData> SponzaMaterial1 = ModelLoader.LoadMaterials("resources/models/radio/radio_mat.json");
        //ModelData SponzaData = ModelLoader.LoadModel("resources/models/Cafe/exterior.json");
        //List<MaterialData> SponzaMaterial = ModelLoader.LoadMaterials("resources/models/Cafe/exterior_mat.json");
        //ModelData SponzaData1 = ModelLoader.LoadModel("resources/models/Cafe/interior.json");
        //List<MaterialData> SponzaMaterial1= ModelLoader.LoadMaterials("resources/models/Cafe/interior_mat.json");
        scene.AddActor(new Actor("Sponza1", SponzaData.ID(), new Vector3f(0,0f,-5f)));
        //JackOfAllTradesActor newJackentity = new JackOfAllTradesActor("Sponza2", radio.ID(), new Vector3f(40.0f, 155.0f, -42.0f),MusicParticle).SetAudio(engineInstance, "resources/EngineResources/audio/sounds/youusedtocallmeonyourcellphone.ogg").SetScale(0.01f);
        //entities.add(newJackentity);
        //scene.AddActor(newJackentity);

        models.add(ModelLoader.LoadModel("resources/models/Metro/OBJ/MetroLeadCar.json"));
        models.add(ModelLoader.LoadModel("resources/models/melona/melona.json"));
        models.add(ModelLoader.LoadModel("resources/models/Alice/Welsh040Alice.json"));
        models.add(ModelLoader.LoadModel("resources/models/Sloop/SloopOBJ.json"));
        models.add(ModelLoader.LoadModel("resources/models/Corvette/corvetteclass.json"));
        for(int i = 0; i < 0; i++) {
            CubeActors.add(new Actor("AdvancedModel" + i, models.get((int)Math.round(Math.min(Math.max(Math.random() *(Math.random() * models.size() - 1),0),models.size() - 1))).ID(), new Vector3f((float)(-200 + Math.random() * 400), (float)(-50 + Math.random() * 100), (float)(250 + Math.random() * -500))));
            angles.add((float)(Math.random() * 360));
            rotatingAngles.add(new Vector3f((float)(Math.random()*2), (float)(Math.random()*2), (float)(Math.random()*2)));
            Velocities.add(new Vector3f((float)(-2 + Math.random()*4), -2 + (float)(Math.random()*4), -2 + (float)(Math.random()*4)));
        }
        CubeActors.forEach(scene::AddActor);

        List<MaterialData> materials = new ArrayList<>();
        materials.addAll(ModelLoader.LoadMaterials("resources/models/Metro/OBJ/MetroLeadCar_mat.json"));
        materials.addAll(ModelLoader.LoadMaterials("resources/models/Corvette/corvetteclass_mat.json"));
        materials.addAll(ModelLoader.LoadMaterials("resources/models/melona/melona_mat.json"));
        materials.addAll(ModelLoader.LoadMaterials("resources/models/Alice/Welsh040Alice_mat.json"));
        materials.addAll(ModelLoader.LoadMaterials("resources/models/Sloop/SloopOBJ_mat.json"));
        materials.addAll(SponzaMaterial);
        materials.addAll(MusicParticleMat);
        materials.addAll(SponzaMaterial1);
        models.add(SponzaData);
        models.add(MusicParticle);
        models.add(radio);
        Camera camera = scene.GetCamera();
        camera.SetPosition(40.0f, 155.0f, -42.0f);
        //camera.SetPosition(0,0,0);
        camera.SetRotation((float) Math.toRadians(10.0f), (float) Math.toRadians(-90.0f),0);
        //camera.SetRotation(0,0,0);

        guiTexture = new GuiTexture("resources/EngineResources/Texture/DefaultTexture.png");
        List<GuiTexture> guiTextures = new ArrayList<>();
        guiTextures.add(guiTexture);

        InitiateAudio(engineInstance);

        return new InitData(models,materials,guiTextures);
    }

    private void InitiateAudio(EngineInstance engineInstance){
        AudioManager audioManager = engineInstance.audioManager();
        Camera camera = engineInstance.scene().GetCamera();
        audioManager.SetAttenuationModel(AL11.AL_EXPONENT_DISTANCE);
        audioManager.SetListener(new SoundListener(camera.GetPosition()));

        SoundBuffer buffer = new SoundBuffer("resources/EngineResources/audio/sounds/youusedtocallmeonyourcellphone.ogg");
        audioManager.AddSoundBuffer(SOUND_BUFFER_PLAYER, buffer);
        AudioSource playerAudioSource = new AudioSource(true,false);
        playerAudioSource.SetPosition(new Vector3f(40.0f, 155.0f, -42.0f));
        playerAudioSource.SetBuffer(buffer.GetBufferID());
        audioManager.AddSoundSource(SOUND_SOURCE_PLAYER,playerAudioSource);
        //playerAudioSource.Play();
        buffer = new SoundBuffer("resources/EngineResources/audio/music/chimken_wing.ogg");
        audioManager.AddSoundBuffer(SOUND_BUFFER_MUSIC,buffer);
        AudioSource source = new AudioSource(true, true);
        source.SetBuffer(buffer.GetBufferID());
        audioManager.AddSoundSource(SOUND_SOURCE_MUSIC,source);
        //source.Play();
    }

    @Override
    public void CameraInput(EngineInstance engineInstance, long FrameDiffNanoSeconds) {
        float Multiplier = MOUSE_SENSITIVITY;
        Scene scene = engineInstance.scene();
        Window window = engineInstance.window();
        Camera camera = scene.GetCamera();

        MouseListener MouseInput = window.getMouseInput();
        Vector2f CurrentMousePos = window.getMouseInput().getCurrentPos();
        Vector2f deltaPos = new Vector2f(0,0);
        deltaPos.x = 0;
        deltaPos.y = 0;
        if (LastMousePos.x >= 0 && LastMousePos.y >= 0 && window.getMouseInput().IsWindowFocused()) {
            deltaPos.x = CurrentMousePos.x - LastMousePos.x;
            deltaPos.y = CurrentMousePos.y - LastMousePos.y;
        }
        LastMousePos.x = CurrentMousePos.x;
        LastMousePos.y = CurrentMousePos.y;
        if(MouseInput.isRightButtonPressed()){

            camera.AddRotation((float)Math.toRadians(-deltaPos.y * Multiplier),(float)Math.toRadians(-deltaPos.x * Multiplier),0);
        }
        engineInstance.audioManager().UpdateListenerPosition(camera);
    }

    @Override
    public void RenderThreadInput(EngineInstance engineInstance, long FrameDiffNanoSeconds) {
        if(ChangeGraphicsSettings){
            if(NextAAMode != -1){
                EngineConfig.getInstance().SetRenderAAType(NextAAMode);
                NextAAMode = -1;
            }
            if((System.nanoTime() - LastFrameTime)/1000000.0 > CoolDown){
                ChangeGraphicsSettings = false;
                LastFrameTime = System.nanoTime();
            }
        } else{
            LastFrameTime = System.nanoTime();
        }
        HandleGui(engineInstance, FrameDiffNanoSeconds);
    }

    @Override
    public void Input(EngineInstance engineInstance, long FrameDiffNanoSeconds) {
        Scene scene = engineInstance.scene();
        Window window = engineInstance.window();

        KeyboardInput input = window.getKeyboardInput();
        float MovementDist = (float)(FrameDiffNanoSeconds / 1000000L) * MOVEMENT_SPEED;
        Camera camera = scene.GetCamera();
        if(input.keyPressed(GLFW_KEY_1)){
            GUI_MODE = 0;
        }
        if(input.keyPressed(GLFW_KEY_2)){
            GUI_MODE = 1;
        }
        if(input.keyPressed(GLFW_KEY_3)){
            GUI_MODE = 2;
        }
        if(input.keyPressed(GLFW_KEY_W)){
            camera.MoveForward(MovementDist);
        }
        if(input.keyPressed(GLFW_KEY_S)){
            camera.MoveBackward(MovementDist);
        }
        if(input.keyPressed(GLFW_KEY_A)){
            camera.MoveLeft(MovementDist);
        }
        if(input.keyPressed(GLFW_KEY_D)){
            camera.MoveRight(MovementDist);
        }
        if(input.keyPressed(GLFW_KEY_SPACE)){
            camera.MoveUp(MovementDist);
        }
        if(input.keyPressed(GLFW_KEY_LEFT_SHIFT)){
            camera.MoveDown(MovementDist);
        }
        if(input.keyPressed(GLFW_KEY_LEFT_CONTROL)){
            camera.Sprint(true);
        } else camera.Sprint(false);
    }

    private boolean HandleGui(EngineInstance engineInstance, long frameTimeNS){
        FetchingMetrics = true;
        ImGuiIO imGuiIO = ImGui.getIO();
        MouseListener mouseListener = engineInstance.window().getMouseInput();
        Vector2f mousePosition = mouseListener.getCurrentPos();
        imGuiIO.addMousePosEvent(mousePosition.x,mousePosition.y);
        imGuiIO.addMouseButtonEvent(0,mouseListener.isLeftButtonPressed());
        imGuiIO.addMouseButtonEvent(1,mouseListener.isRightButtonPressed());
        int windowFlags = ImGuiWindowFlags.NoDecoration
                | ImGuiWindowFlags.AlwaysAutoResize
                | ImGuiWindowFlags.NoSavedSettings
                | ImGuiWindowFlags.NoFocusOnAppearing
                | ImGuiWindowFlags.NoNav
                | ImGuiWindowFlags.NoMove;
        ImGui.newFrame();
        if(GUI_MODE == 0){
            double FrameTimeMS = (frameTimeNS/1000000.0);
            double AverageGPUTimeMS = EngineConfig.LAST_AVERAGE_GPU_TIME/1000000.0;
            ImGui.setNextWindowPos(0,0, ImGuiCond.Always);
            //ImGui.setNextWindowSize(300,100);
            ImGui.begin("FPS overlay",windowFlags);
            ImGui.text("PERFORMANCE METRICS:");
            ImGui.separator();
            ImGui.text(String.format("Vulkan FPS: " + EngineConfig.FrameRate_RENDERTHREAD + "fps | FrameTime: %.2f" + "ms | Avg. GPU Time: %.3f" + "ms | TPS: " + EngineConfig.TPS_RENDERTHREAD,FrameTimeMS,AverageGPUTimeMS));
            ImGui.text(String.format("Primary Thread FPS: " + EngineConfig.FrameRate_PRIMARYTHREAD + "fps | GLFW Window FrameRate:" + EngineConfig.FrameRate_WINDOW + "fps | TPS: " + EngineConfig.TPS_PRIMARYTHREAD));
            ImGui.text(String.format("Main Thread FPS: " + EngineConfig.FrameRate_MAINENGINETHREAD + "fps | TPS: " + EngineConfig.TPS_MAINENGINETHREAD));
            ImGui.text(String.format("Fast Thread FPS: " + EngineConfig.FrameRate_FASTENGINETHREAD + "fps | TPS: " + EngineConfig.TPS_FASTENGINETHREAD));
            ImGui.separator();
            ImGui.text("GRAPHICS SETTINGS:");
            ImGui.text(" ");
            ImGui.text("anti aliasing:");
            if(ImGui.button("No AA")){
                ChangeGraphicsSettings = true;
                CoolDown = 2000;
                NextAAMode = (0);
            }
            ImGui.sameLine();
            if(ImGui.button("FXAA")){
                ChangeGraphicsSettings = true;
                CoolDown = 2000;
                NextAAMode = (1);
            }
            ImGui.sameLine();
            if(ImGui.button("MSAAx2")){
                ChangeGraphicsSettings = true;
                CoolDown = 2000;
                NextAAMode = (2);
            }
            ImGui.sameLine();
            if(ImGui.button("MSAAx4")){
                ChangeGraphicsSettings = true;
                CoolDown = 2000;
                NextAAMode = (3);
            }
            ImGui.sameLine();
            if(ImGui.button("MSAAx8")){
                ChangeGraphicsSettings = true;
                CoolDown = 2000;
                NextAAMode = 4;
            }
            ImGui.text("pipeline:");
            if(ImGui.button("Single Pass")){
                ChangeGraphicsSettings = true;
                CoolDown = 500;
                SceneRender.SetDualPassRendering(false);
            }
            ImGui.sameLine();
            if(ImGui.button("dual Pass")){
                ChangeGraphicsSettings = true;
                CoolDown = 500;
                SceneRender.SetDualPassRendering(true);
            }
            if(ImGui.button("AlphaToCoverage False")){
                ChangeGraphicsSettings = true;
                CoolDown = 500;
                EngineConfig.getInstance().SetAlphaToCoverage(false);
                PrimaryRuntime.GetRenderThread().GetRenderer().GetSceneRender().RebuildPipelines(PrimaryRuntime.GetRenderThread().GetRenderer().GetVulkanContext());
            }
            ImGui.sameLine();
            if(ImGui.button("AlphaToCoverage True")){
                ChangeGraphicsSettings = true;
                CoolDown = 500;
                EngineConfig.getInstance().SetAlphaToCoverage(true);
                PrimaryRuntime.GetRenderThread().GetRenderer().GetSceneRender().RebuildPipelines(PrimaryRuntime.GetRenderThread().GetRenderer().GetVulkanContext());
            }
            ImGui.separator();
            ImGui.text("Level Tools:");
            if(ImGui.button("Spawn Radio")){
                JackOfAllTradesActor newJackentity = new JackOfAllTradesActor("Radio" + entities.size(), radio.ID(), new Vector3f(engineInstance.scene().GetCamera().GetPosition()),MusicParticle).SetAudio(engineInstance, "resources/EngineResources/audio/sounds/youusedtocallmeonyourcellphone.ogg").SetScale(0.01f);
                entities.add(newJackentity);
                engineInstance.scene().AddActor(newJackentity);
            }
            if(PerformanceMetrics.size() > 0) {
                ImGui.text("  ");
                ImGui.separator();
                for (int i = 0; i < PerformanceMetrics.size(); i++) {

                    String[] memoryUsageMetrics = PerformanceMetrics.get(i);
                    for(int j = 0; j < memoryUsageMetrics.length; j++){
                        ImGui.text(memoryUsageMetrics[j]);
                    }
                    ImGui.separator();
                    ImGui.text("  ");
                }
            }
            ImGui.end();
        } else if (GUI_MODE == 1){
            ImGui.setNextWindowPos(0,0, ImGuiCond.Always);
            ImGui.setNextWindowSize(500,500);
            ImGui.begin("Test Window");
            ImGui.image(guiTexture.ID(),new ImVec2(300,300));
            ImGui.end();
        } else if (GUI_MODE == 2){
        }
        if (ChangeGraphicsSettings){
            ImGuiViewport viewport = ImGui.getMainViewport();
            float CenterX = viewport.getWorkPosX() + viewport.getWorkSizeX() / 2.0f;
            float CenterY = viewport.getWorkPosY() + viewport.getWorkSizeY() / 2.0f;
            ImGui.setNextWindowPos(CenterX,CenterY, ImGuiCond.Always,0.5f,0.5f);
            ImGui.begin("change notification",windowFlags);
            ImGui.setWindowFontScale(4.0f);
            ImGui.text("APPLYING GRAPHICS SETTINGS...");
            ImGui.end();
        }
        ImGui.endFrame();
        ImGui.render();
        FetchingMetrics = false;
        return imGuiIO.getWantCaptureKeyboard();
    }

    @Override
    public void Update(EngineInstance engineInstance, long FrameDiffNanoSeconds) {
        float DeltaTime = (float)(FrameDiffNanoSeconds/16000000.0);
        for(int i = 0; i < angles.size()/2; i++) {

        }
        Ticks++;
        if(Ticks == 1000){

        }
    }

    @Override
    public void UpdateFastThread(EngineInstance engineInstance, long FrameDiffNanoSeconds) {
        float DeltaTime = (float)(FrameDiffNanoSeconds/16000000.0);
        for(int i = 0; i < angles.size(); i++) {
            angles.set(i, (angles.get(i) + 1.0f* DeltaTime) % 360);
            CubeActors.get(i).GetRotation().identity().rotateAxis((float) EngineConfig.DegToRad * angles.get(i), rotatingAngles.get(i));
           CubeActors.get(i).GetPosition().add((Velocities.get(i).x/100.0f) * DeltaTime,(Velocities.get(i).y/100.0f) * DeltaTime,(Velocities.get(i).z/100.0f) * DeltaTime);
           CubeActors.get(i).UpdateModelMatrix();
        }
        for(int i = 0; i < entities.size(); i++){
            JackOfAllTradesActor entity = entities.get(i);
            entity.tick(engineInstance.scene());
        }
    }

    @Override
    public void SecondUpdate(EngineInstance engineInstance) {
        SettingPerformanceMetrics.clear();
        List<String[]> Metrics = new ArrayList<>();
        String[] SceneDetails = new String[5];
        Vector3f Position = engineInstance.scene().GetCamera().GetPosition();
        Vector3f Rotation = engineInstance.scene().GetCamera().GetRotation();
        SceneDetails[0] = String.format("INSTANCE INFORMATION: ");
        SceneDetails[1] = String.format("  Camera Position: X: %.2f Y: %.2f Z: %.2f",Position.x ,Position.y ,Position.z);
        SceneDetails[2] = String.format("  Camera Rotation: X: %.2f° Y: %.2f° Z: %.2f°",Rotation.x *EngineConfig.RadToDeg,Rotation.y *EngineConfig.RadToDeg ,Rotation.z *EngineConfig.RadToDeg);
        SceneDetails[3] = String.format("  Anti Aliasing: " + EngineConfig.getInstance().RenderAAType().name());
        SceneDetails[4] = String.format("  FOV: %.2f°",(EngineConfig.getInstance().GetFOV()*EngineConfig.RadToDeg));
        String[] DeviceInfo = new String[5];
        DeviceInfo[0] = String.format("SYSTEM INFORMATION: ");
        DeviceInfo[1] = String.format("  CPU: " + EngineConfig.getInstance().GetCPUDetails());
        DeviceInfo[2] = String.format("  RAM: " + CPUMonitor.RamSize);
        DeviceInfo[3] = String.format("  GPU: " + EngineConfig.getInstance().GetGPU());
        DeviceInfo[4] = String.format("  VRAM: " + GPUProfiler.GetVramCapacity());
        Metrics.add(SceneDetails);
        Metrics.add(DeviceInfo);
        SettingPerformanceMetrics.addAll(Metrics);
        SettingPerformanceMetrics.addAll(EngineConfig.getInstance().GetMemoryProfiling());
        SettingPerformanceMetrics.addAll(EngineConfig.getInstance().GetSystemProfiling());
        PerformanceMetrics = List.copyOf(SettingPerformanceMetrics);
    }
}
