package speiger.src.coreengine.application;

import java.text.DecimalFormat;

import org.lwjgl.opengl.GL11;

import speiger.src.collections.objects.queues.ObjectArrayFIFOQueue;
import speiger.src.collections.objects.queues.ObjectPriorityQueue;
import speiger.src.coreengine.rendering.inputOld.window.Window;
import speiger.src.coreengine.rendering.utils.AllocationTracker;
import speiger.src.coreengine.rendering.utils.GLStamper;
import speiger.src.coreengine.rendering.utils.GLStamper.GLStamp;
import speiger.src.coreengine.rendering.utils.GLStateTracker;
import speiger.src.coreengine.utils.counters.averager.TimeAverager;
import speiger.src.coreengine.utils.counters.timers.FPSTimer;
import speiger.src.coreengine.utils.counters.timers.FrameSyncer;
import speiger.src.coreengine.utils.counters.timers.TimerTarget;
import speiger.src.coreengine.utils.helpers.IOUtils;
import speiger.src.coreengine.utils.helpers.TextUtil;
import speiger.src.coreengine.utils.io.GameLog;
import speiger.src.coreengine.utils.io.GameLog.LogLevel;
import speiger.src.coreengine.utils.profiler.IProfiler;

public class ApplicationExecutor
{
	public static final DecimalFormat FORMATTER = new DecimalFormat("###");
	FPSTimer timer = new FPSTimer();
	TimerTarget fps = timer.getFPS();
	TimerTarget ups = timer.getUPS();
	FrameSyncer sync = new FrameSyncer();
	
	TimeAverager gpuTime = new TimeAverager();
	ObjectPriorityQueue<GLStamp> stamps = new ObjectArrayFIFOQueue<>();
	StringBuilder builder = new StringBuilder();
	
    long frame = 0L;
    
    Application owner;
    
	public ApplicationExecutor(Application owner)
	{
		this.owner = owner;
		owner.timer = timer;
	}
    
	private IProfiler gpu()
	{
		return owner.gpuProfiler;
	}
	
	public void start(Window window)
	{
		createTimeHacker();
		timer.init();
		try
		{
			while(window.shouldRun()) loop(window);
		}
		catch(Exception e)
		{
			GameLog.error("Client Has Crashed", e, LogLevel.ERROR);
		}
	}
	
	private void loop(Window window)
	{
		GLStamp stamp = GLStamper.INSTANCE.createStamp("MainStamp").start();
		stamps.enqueue(stamp);
		gpu().start("Client").start("Update");
		window.update();
		for(long f = timer.getDelta();f >= ups.getTargetNS();f = timer.consumeDelta())
		{
			ups.tick();
			owner.updateInternal();
			builder.setLength(0);
			builder.append(owner.getMainWindowName());
			if(owner.showProfilingInfo) addDebugInfo(builder);
			window.setTitle(builder.toString());
		}
		gpu().next("Render");
		render(timer.getParticalTime());
		gpu().next("Trackers");
		boolean end = timer.update();
		stamp.stop();
		AllocationTracker.INSTANCE.update();
		GLStateTracker.instance().onFrameEnded();
		gpu().next("Window").start("V-Sync");
		window.finishFrame();
		if(window.isCPULimited()) sync.sync(fps.getTargetTicks());
		gpu().next("Input");
		window.gatherInputs();
		gpu().stop().stop().stop().onFrameEnded(end);
		while(!stamps.isEmpty() && stamps.first().isFinished())
		{
			stamp = stamps.dequeue();
			gpuTime.addEntry(stamp.getResult());
			stamp.release();
		}
		frame++;
	}
	
	private void render(float particalTicks)
	{
		gpu().start("Prep");
		fps.tick();
		GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
		gpu().stop();
		owner.renderInternal(particalTicks);
	}
	
	public void createTimeHacker()
	{
        Thread thread = new Thread(() -> {
        	while (true){
                try {Thread.sleep(2147483647L);}
                catch (InterruptedException e){}
            }
        }, "Time Fixer Thread");
        thread.setDaemon(true);
        thread.start();
	}
	
	protected void addDebugInfo(StringBuilder builder)
	{
		builder.append("(");
		builder.append(fps.getTicks()).append(":").append(ups.getTicks());
		owner.addExtraTickRates(T -> builder.append(":").append(T));
		builder.append(",").append(getMemoryUsage());
		builder.append(",").append("OffHeap: "+(AllocationTracker.INSTANCE.getOffHeapMemory() >> 20)+"MB");
		builder.append(",").append("CPU-A: "+IOUtils.convertBytes(AllocationTracker.INSTANCE.getCPUAllocatedBytes()));
		builder.append(",").append("GPU-A: "+IOUtils.convertBytes(AllocationTracker.INSTANCE.getGPUAllocatedBytes()));
		builder.append(",").append(TextUtil.convertTime(timer.getUsage(), "Client:", FORMATTER));
		builder.append(",").append(TextUtil.convertTime(gpuTime.getAverage(), "GPU:", FORMATTER));
		owner.addExtraTimers((N, T) -> builder.append(",").append(TextUtil.convertTime(T, N, FORMATTER)));
		builder.append(")");
	}
	
	private String getMemoryUsage()
	{
		Runtime time = Runtime.getRuntime();
		long aviable = time.totalMemory();
		return "Memory:"+((aviable - time.freeMemory()) >> 20)+"MB/"+(aviable >> 20)+"MB";
	}
}
