package speiger.src.coreengine.application;

import java.nio.file.Path;
import java.util.List;
import java.util.function.IntConsumer;
import java.util.function.ObjLongConsumer;

import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;

import speiger.src.collections.objects.utils.ObjectLists;
import speiger.src.coreengine.assets.AssetManager;
import speiger.src.coreengine.assets.base.IAssetPackage;
import speiger.src.coreengine.assets.reloader.ResourceReloader;
import speiger.src.coreengine.rendering.guiOld.GuiManager;
import speiger.src.coreengine.rendering.guiOld.base.DebugOverlay;
import speiger.src.coreengine.rendering.guiOld.renderer.provider.FontManager;
import speiger.src.coreengine.rendering.inputOld.Keyboard;
import speiger.src.coreengine.rendering.inputOld.Mouse;
import speiger.src.coreengine.rendering.inputOld.camera.Camera;
import speiger.src.coreengine.rendering.inputOld.window.Window;
import speiger.src.coreengine.rendering.inputOld.window.WindowProvider;
import speiger.src.coreengine.rendering.shaderOld.ProjectionBuffer;
import speiger.src.coreengine.rendering.shaderOld.ShaderTracker;
import speiger.src.coreengine.rendering.texturesOld.base.TextureManager;
import speiger.src.coreengine.rendering.utils.Cursor;
import speiger.src.coreengine.utils.counters.timers.FPSTimer;
import speiger.src.coreengine.utils.eventbus.EventBus;
import speiger.src.coreengine.utils.helpers.IOUtils;
import speiger.src.coreengine.utils.profiler.GPUProfiler;
import speiger.src.coreengine.utils.profiler.IProfiler;
import speiger.src.coreengine.utils.profiler.Profiler;

public abstract class Application
{
	private static Application INSTANCE;
	protected Path applicationFolder;
	protected FPSTimer timer;
	protected ApplicationExecutor executor = new ApplicationExecutor(this);
	protected IProfiler clientProfiler = new Profiler("Client Thread");
	protected IProfiler gpuProfiler = new GPUProfiler("GPU", clientProfiler);
	protected boolean showProfilingInfo = true;
	protected WindowProvider provider = new WindowProvider();
	protected Window mainWindow;
	protected Camera camera;
	protected EventBus eventBus = new EventBus();
	
	protected ResourceReloader reloader = new ResourceReloader();
	protected AssetManager assetManager;
	protected FontManager fonts = new FontManager();
	protected ProjectionBuffer projectionBuffer;
	protected GuiManager uiManager;
	
	protected Application() {
		INSTANCE = this;
	}
	
	public static Application getInstance() {
		return INSTANCE;
	}
	
	public void run()
	{
		GLFWErrorCallback.createPrint(System.err).set();
		if(!GLFW.glfwInit()) throw new IllegalStateException("OpenGL can't be loaded");
		provider.init();
		boolean initEarly = earlyUILoad();
		try
		{
			mainWindow = createWindow(provider);
			if(initEarly) mainWindow.finishWindow();
		}
		catch(Exception e)
		{
			System.err.println("Could not create a Window!");
			e.printStackTrace();
			if(mainWindow != null) mainWindow.destroy();
			provider.destroy();
			System.exit(0);
			return;
		}
		Thread.currentThread().setName("Client Thread");
		applicationFolder = IOUtils.getBaseLocation();
		applicationFolder = IOUtils.endsWith(applicationFolder, ".jar") ? applicationFolder : Path.of("bin/main");
		internalInit();
		init(applicationFolder);
		if(!initEarly) mainWindow.finishWindow();
		executor.start(mainWindow);
		mainWindow.destroy();
		reloader.deleteResources();
		destroy();
		System.exit(0);
	}
	
	protected void internalInit() {
		assetManager = new AssetManager(gatherPackages());
		reloader.addReloadableResource(fonts);
		fonts.setAssetManager(assetManager);
		ShaderTracker.INSTANCE.init(assetManager);
		TextureManager.INSTANCE.init(assetManager);
		preinit();
		reloader.addReloadableResource(ShaderTracker.INSTANCE);
		reloader.addReloadableResource(TextureManager.INSTANCE);
		camera = new Camera(mainWindow);
		if(initUI()) uiManager = BaseUIManager.create(this);
		Keyboard.INSTANCE.init(eventBus, mainWindow);
		Mouse.INSTANCE.init(eventBus, mainWindow, camera);
		reloader.addReloadableResource(Cursor.INSTANCE, true);
		mainWindow.addListener(camera.getFrustrum(), true);
		projectionBuffer = new ProjectionBuffer(camera, mainWindow);
	}
	
	public List<IAssetPackage> gatherPackages() { return ObjectLists.singleton(IAssetPackage.of(applicationFolder)); }
	public abstract String getMainWindowName();
	public void addExtraTickRates(IntConsumer ticks) {};
	public void addExtraTimers(ObjLongConsumer<String> profiler) {};
	public boolean initUI() { return true; }
	public boolean earlyUILoad() { return true; }
	public DebugOverlay createCustomDebug() { return null; }
	public abstract Window createWindow(WindowProvider provider) throws Exception;
	public void preinit() {}
	public abstract void init(Path path);
	public void preUpdate() {}
	public abstract void update();
	public abstract void render(float particalTicks);
	public abstract void destroy();
	
	protected final void updateInternal()
	{
		preUpdate();
		if(uiManager != null) 
		{
			clientProfiler.start("UI");
			uiManager.onFixedUpdate();
			clientProfiler.stop();
		}
		clientProfiler.start("Input");
		camera.onInput();
		Mouse.update();
		clientProfiler.stop();
		update();
	}
	
	protected final void renderInternal(float particalTicks) {
		render(particalTicks);
		if(uiManager != null)
		{
			gpuProfiler.start("UI");
			uiManager.render(particalTicks);
			gpuProfiler.stop();
		}
		gpuProfiler.start("camera");
		camera.update(particalTicks);
		gpuProfiler.stop();
	}
	
	public AssetManager getAssetManager() { return assetManager; }
	public ResourceReloader getReloader() { return reloader; }
	public Window getMainWindow() { return mainWindow; }
	public Camera getCamera() { return camera; }
	public EventBus getEventBus() { return eventBus; }
	public ProjectionBuffer getProjectionBuffer() { return projectionBuffer; }
	public FPSTimer getTimer() { return timer; }
	public FontManager getFonts() { return fonts; }
	public GuiManager getUiManager() { return uiManager; }
	public IProfiler getClientProfiler() { return clientProfiler; }
	public IProfiler getGPUProfiler() { return gpuProfiler; }
	public long getFrame() { return executor.frame; }
}