package com.bitfire.uracer.game.debug;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.WindowedMean;
import com.badlogic.gdx.utils.TimeUtils;
import com.bitfire.uracer.URacer;
import com.bitfire.uracer.configuration.Config;
import com.bitfire.uracer.utils.AMath;

public final class DebugStatistics {
	// public statistical data
	public WindowedMean meanPhysics = new WindowedMean(16);
	public WindowedMean meanRender = new WindowedMean(16);
	public WindowedMean meanTickCount = new WindowedMean(16);

	// internal data for graphics representation
	private Pixmap pixels;
	private Texture texture;
	private TextureRegion region;
	private int PanelWidth;
	private int PanelHeight;
	private float ratio_rtime, ratio_ptime, ratio_fps;

	// internal timing data
	private long startTime;
	private long intervalNs;

	// internal stats data
	private float[] dataRenderTime;
	private float[] dataFps;
	private float[] dataPhysicsTime;
	private float[] dataTimeAliasing;

	public DebugStatistics () {
		init(100, 50, 0.2f);
	}

	public DebugStatistics (int width, int height, float updateHz) {
		init(width, height, updateHz);
	}

	public DebugStatistics (float updateHz) {
		init(100, 50, updateHz);
	}

	private void init (int width, int height, float updateHz) {
		// assert (width < 256 && height < 256);

		final float oneOnUpdHz = 1f / updateHz;

		PanelWidth = width;
		PanelHeight = height;
		intervalNs = (long)(1000000000L * oneOnUpdHz);

		pixels = new Pixmap(PanelWidth, PanelHeight, Format.RGBA8888);
		texture = new Texture(width, height, Format.RGBA8888);
		texture.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
		region = new TextureRegion(texture, 0, 0, pixels.getWidth(), pixels.getHeight());

		// create data
		dataRenderTime = new float[PanelWidth];
		dataFps = new float[PanelWidth];
		dataPhysicsTime = new float[PanelWidth];
		dataTimeAliasing = new float[PanelWidth];

		// precompute constants
		ratio_rtime = ((float)PanelHeight / 2f) * Config.Physics.TimestepHz;
		ratio_ptime = ((float)PanelHeight / 2f) * Config.Physics.TimestepHz;
		ratio_fps = ((float)PanelHeight / 2f) * oneOnUpdHz;

		reset();
	}

	public void dispose () {
		pixels.dispose();
		texture.dispose();
	}

	private void reset () {
		for (int i = 0; i < PanelWidth; i++) {
			dataRenderTime[i] = 0;
			dataPhysicsTime[i] = 0;
			dataFps[i] = 0;
			dataTimeAliasing[i] = 0;
		}

		plot();
		startTime = TimeUtils.nanoTime();
	}

	public TextureRegion getRegion () {
		return region;
	}

	public int getWidth () {
		return PanelWidth;
	}

	public int getHeight () {
		return PanelHeight;
	}

	public void update () {
		if (collect()) {
			plot();
		}
	}

	private void plot () {
		// background
		pixels.setColor(0, 0, 0, 0.8f);
		pixels.fill();

		float alpha = 0.5f;
		for (int x = 0; x < PanelWidth; x++) {
			int xc = PanelWidth - x - 1;
			int value = 0;

			// render time
			value = (int)(dataRenderTime[x] * ratio_rtime);
			if (value > 0) {
				pixels.setColor(0, 0.5f, 1f, alpha);
				pixels.drawLine(xc, 0, xc, value);
			}

			// physics time
			value = (int)(dataPhysicsTime[x] * ratio_ptime);
			pixels.setColor(1, 1, 1, alpha);
			pixels.drawLine(xc, 0, xc, value);

			// fps
			value = (int)(dataFps[x] * ratio_fps);
			if (value > 0) {
				pixels.setColor(0, 1, 1, .8f);
				pixels.drawPixel(xc, value);
			}

			// time aliasing
			value = (int)(AMath.clamp(dataTimeAliasing[x] * PanelHeight, 0, PanelHeight));
			if (value > 0) {
				pixels.setColor(1, 0, 1, .8f);
				pixels.drawPixel(xc, value);
			}
		}

		texture.draw(pixels, 0, 0);
	}

	private boolean collect () {
		long time = TimeUtils.nanoTime();

		if (time - startTime > intervalNs) {
			// shift values
			for (int i = PanelWidth - 1; i > 0; i--) {
				dataRenderTime[i] = dataRenderTime[i - 1];
				dataPhysicsTime[i] = dataPhysicsTime[i - 1];
				dataFps[i] = dataFps[i - 1];
				dataTimeAliasing[i] = dataTimeAliasing[i - 1];
			}

			meanPhysics.addValue(URacer.Game.getPhysicsTime());
			meanRender.addValue(URacer.Game.getRenderTime());
			meanTickCount.addValue(URacer.Game.getLastTicksCount());

			dataPhysicsTime[0] = meanPhysics.getMean();
			dataRenderTime[0] = meanRender.getMean();
			dataFps[0] = Gdx.graphics.getFramesPerSecond();
			dataTimeAliasing[0] = URacer.Game.getTemporalAliasing();

			startTime = time;
			return true;
		}

		return false;
	}
}