package zero1hd.rhythmbullet.audio.visualizer;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Disposable;

import zero1hd.rhythmbullet.audio.MusicController;

public class DoubleHorizontalVisualizer implements Disposable {
	private int width, height, barWidth, spaceBetweenBars;
	private int x, y;
	private ShapeRenderer shapeRenderer;
	private PCMSystem pcm;
	private float[] amplitudes;
	private int[] barHeights;
	private int binsPerBar;
	private float offset;
	private int boundaryThickness;
	private boolean debug = false;
	private boolean significantBeat;
	private float maxAverageAmplitude;
	private byte significantFrames;
	private byte requiredSignificantFrames;
	private float targetDelta;
	private float significantThreshold = 0.5f;
	private float spacePercentage = 0.7f;
	private float baseSensitivity = 0.009f;
	private int barCount = 120;
	private float barChangeRate = 6.5f;
	private int smoothRange = 2;
	private int binsToInclude = 120;
	private Color color = new Color(0.5f, 0.6f, 0.8f, 0.46f);
	private int averageAmplitude;

	/**
	 * 
	 * @param barCount amount of bars this visualizer should have.
	 * @param width the width of the visualizer.
	 * @param spacePercentage the percentage of a bar that should be space.
	 */
	public DoubleHorizontalVisualizer(int width, int height, float heightSensitivity, int boundaryThickness, int targetFPS, MusicController musicController, PCMSystem PCMSystem) {
		this.barWidth = width/barCount;
		this.spaceBetweenBars = MathUtils.round(barWidth * spacePercentage);
		this.barWidth -= spaceBetweenBars;
		this.baseSensitivity *= heightSensitivity;
		pcm = PCMSystem;
		binsPerBar = (binsToInclude/barCount);
		this.width = width;
		this.height = height;
		amplitudes = new float[barCount];
		barHeights = new int[barCount];
		shapeRenderer = new ShapeRenderer();
		boundaryThickness = barWidth;
		offset = (width - (barCount*(barWidth+spaceBetweenBars)-spaceBetweenBars))/2f + x;
		this.targetDelta = 1f/targetFPS;
	}
	
	public void act(float delta) {
		if (pcm.hasAudioChanged()) {
			maxAverageAmplitude = 0;
			averageAmplitude = 0;
			significantBeat = false;
		}
		float[] freqBins = pcm.getFrequencyBins();
		averageAmplitude = 0;
		for (int bar = 0; bar < amplitudes.length; bar++) {
			amplitudes[bar] = 0;
			for (int freq = bar*binsPerBar; freq < (bar*binsPerBar) + binsPerBar; freq++) {
				amplitudes[bar] += Math.abs(freqBins[freq]) * baseSensitivity;
			}
			amplitudes[bar] /= binsPerBar;
			
			averageAmplitude += amplitudes[bar];
		}
		
		averageAmplitude /= amplitudes.length;
		if (averageAmplitude > maxAverageAmplitude) {
			maxAverageAmplitude = averageAmplitude;
		}
		if (averageAmplitude > maxAverageAmplitude*significantThreshold && !significantBeat) {
			if (maxAverageAmplitude > 0) {
				significantFrames++;
				if (significantFrames >= requiredSignificantFrames) {
					significantFrames = 0;
					significantBeat = true;
				}
			}
		} else {
			if (significantBeat) {
				significantBeat = false;
				requiredSignificantFrames = 16;
			} else {
				requiredSignificantFrames = 4;
			}
			significantFrames = 0;
		}
		
		for (int bar = 0; bar < barHeights.length; bar++) {
			int smoothCount = 1;
			for (int range = 0; range < smoothRange; range++) {
				if (bar+range < amplitudes.length) {
					amplitudes[bar] += amplitudes[bar+range];
					smoothCount++;
				}
				if (bar-range > 0) {
					amplitudes[bar] += amplitudes[bar-range];
					smoothCount++;
				}
			}
			amplitudes[bar] /= smoothCount;
			
			
			int pixelsMoved = 0;
			int difference = MathUtils.round(amplitudes[bar] - barHeights[bar]);
			pixelsMoved = MathUtils.floor(difference*targetDelta*barChangeRate);
			if (pixelsMoved >= 0) {
				if (barHeights[bar] + pixelsMoved > amplitudes[bar]) {
					barHeights[bar] += MathUtils.round(difference*targetDelta);
				} else {
					barHeights[bar] += pixelsMoved;
				}
			} else {
				if (barHeights[bar] + pixelsMoved < amplitudes[bar]) {
					barHeights[bar] += MathUtils.round(difference*targetDelta);
				} else {
					barHeights[bar] += pixelsMoved;
				}
			}
			
		}
	}
	
	public void draw(Batch batch, float parentAlpha) {
		batch.end();
		Gdx.gl.glEnable(GL20.GL_BLEND);
		shapeRenderer.begin(ShapeType.Filled);
		shapeRenderer.setProjectionMatrix(batch.getProjectionMatrix());
		shapeRenderer.setTransformMatrix(batch.getTransformMatrix());
		if (boundaryThickness > 0) {
			shapeRenderer.rect(x, y, width, boundaryThickness);
			shapeRenderer.rect(x, y+height, width, -boundaryThickness);
		}
		
		for (int bar = 0; bar < barCount; bar++) {
			shapeRenderer.setColor(color);
			shapeRenderer.rect(offset + (spaceBetweenBars+barWidth)*bar, y+height, barWidth, barHeights[bar]);
			shapeRenderer.rect(offset + (spaceBetweenBars+barWidth)*bar, y, barWidth, -barHeights[barHeights.length - 1 - bar]);
		}
		if (debug) {
			shapeRenderer.setColor(Color.RED);
			shapeRenderer.rect(0, maxAverageAmplitude+y, width, 1);
			if (significantBeat) {
				shapeRenderer.setColor(Color.GREEN);
			} else {
				shapeRenderer.setColor(Color.WHITE);
			}
			shapeRenderer.rect(0, averageAmplitude+y, width, 1);
			shapeRenderer.setColor(Color.YELLOW);
			shapeRenderer.rect(0, maxAverageAmplitude*significantThreshold + y, width, 1);
		}
		shapeRenderer.end();
		Gdx.gl.glDisable(GL20.GL_BLEND);
		batch.begin();
	}
	
	public int getWidth() {
		return width;
	}
	
	public int getHeight() {
		return height;
	}
	
	public void setX(int x) {
		this.x = x;
	}
	
	public void setY(int y) {
		this.y = y;
	}
	
	public void setPosition(int x, int y) {
		this.x = x;
		this.y = y;
	}
	
	public int getX() {
		return x;
	}
	
	public int getY() {
		return y;
	}
	
	public boolean isSignificantBeat() {
		return significantBeat;
	}
	
	public void updateMusic() {
		pcm.loadMusic();
	}
	
	@Override
	public void dispose() {
		pcm.dispose();
	}
}
