// custom build and feature flags
#ifdef DEBUG
	#define OPENGL_DEBUG        0
	#define FULLSCREEN          0
	#define DESPERATE           0
	#define BREAK_COMPATIBILITY 0
#else
	#define OPENGL_DEBUG        0
	#define FULLSCREEN          0
	#define DESPERATE           0
	#define BREAK_COMPATIBILITY 0
#endif

#define POST_PASS    0
#define USE_MIPMAPS  1
#define USE_AUDIO    1
#define NO_UNIFORMS  0

#include "definitions.h"
#if OPENGL_DEBUG
	#include "debug.h"
#endif

#include "glext.h"
#pragma data_seg(".shader")
#include "shaders/fragment.inl"
#if POST_PASS
	#pragma data_seg(".shader")
	#include "shaders/post.inl"
#endif

#include "fft.h"

#pragma data_seg(".pids")
// static allocation saves a few bytes
static int pidMain;
static int pidPost;
// static HDC hDC;

#ifndef EDITOR_CONTROLS
#pragma code_seg(".main")
void entrypoint(void)
#else
#include "editor.h"

int __cdecl main(int argc, char* argv[])
#endif

{
	// initialize window
	#if FULLSCREEN
		ChangeDisplaySettings(&screenSettings, CDS_FULLSCREEN);
		ShowCursor(0);
		HWND window = CreateWindow((LPCSTR)0xC018, 0, WS_POPUP | WS_VISIBLE | WS_MAXIMIZE, 0, 0, 0, 0, 0, 0, 0, 0);
		const HDC hDC = GetDC(window);
	#else
		#ifdef EDITOR_CONTROLS
			HWND window = CreateWindow("static", 0, WS_POPUP | WS_VISIBLE, 0, 0, XRES, YRES, 0, 0, 0, 0);
			HDC hDC = GetDC(window);
		#else
			// you can create a pseudo fullscreen window by similarly enabling the WS_MAXIMIZE flag as above
			// in which case you can replace the resolution parameters with 0s and save a couple bytes
			// this only works if the resolution is set to the display device's native resolution
			HWND window = CreateWindow((LPCSTR)0xC018, 0, WS_POPUP | WS_VISIBLE, 0, 0, XRES, YRES, 0, 0, 0, 0);
			HDC hDC = GetDC(window);
		#endif
	#endif

	// initalize opengl context
	SetPixelFormat(hDC, ChoosePixelFormat(hDC, &pfd), &pfd);
	wglMakeCurrent(hDC, wglCreateContext(hDC));

	// create and compile shader programs
	pidMain = ((PFNGLCREATESHADERPROGRAMVPROC)wglGetProcAddress("glCreateShaderProgramv"))(GL_FRAGMENT_SHADER, 1, &fragment_frag);
	#if POST_PASS
		pidPost = ((PFNGLCREATESHADERPROGRAMVPROC)wglGetProcAddress("glCreateShaderProgramv"))(GL_FRAGMENT_SHADER, 1, &post_frag);
	#endif

	#ifdef SU_LOAD_GMDLS
		su_load_gmdls();
	#endif // SU_LOAD_GMDLS
	// initialize sound
	#ifndef EDITOR_CONTROLS
		#if USE_AUDIO
		LPDIRECTSOUND direct_sound;
		LPDIRECTSOUNDBUFFER direct_sound_buffer;
		DirectSoundCreate(0, &direct_sound, 0);

		IDirectSound_SetCooperativeLevel(direct_sound, window, DSSCL_PRIORITY);
		IDirectSound_CreateSoundBuffer(direct_sound, &buffer_description, &direct_sound_buffer, NULL);

		LPVOID p1;
		DWORD l1;
		IDirectSoundBuffer_Lock(direct_sound_buffer, 0, SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE, &p1, &l1, NULL, NULL, 0);

		CreateThread(0, 0, (LPTHREAD_START_ROUTINE)su_render_song, p1, 0, 0);

		#endif

	#else
		Leviathan::Editor editor = Leviathan::Editor();
		editor.updateShaders(&pidMain, &pidPost, true);

		// absolute path always works here
		// relative path works only when not ran from visual studio directly
		Leviathan::Song track(L"audio.wav");
		track.play();
		double position = 0.0;
	#endif

	long playCursor = 0;
	long lastPlayCursor = -1;
	volatile float maximum = 0.0; // Helper variable to calculate maximum fft output for normalization
    
	// Unlock buffer for next use
	IDirectSoundBuffer_Unlock(direct_sound_buffer, p1, SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE, NULL, NULL);
	
	// Play sound
    direct_sound_buffer->Play(0, 0, 0);

	static float syncs[1 + SU_NUMSYNCS];

	// Init FFT
	init_hamming_window();
	// FFT buffers
	static float fft_input[FFT_SIZE];
	static float fft_output[FFT_SIZE / 2]; // Magnitudes
	static float fft_uniform[FFT_SIZE / 4];
	
	do
	{
		direct_sound_buffer->GetCurrentPosition((DWORD*)&playCursor, NULL);

		#if !(DESPERATE)
			// do minimal message handling so windows doesn't kill your application
			// not always strictly necessary but increases compatibility and reliability a lot
			// normally you'd pass an msg struct as the first argument but it's just an
			// output parameter and the implementation presumably does a NULL check
			PeekMessage(0, 0, 0, 0, PM_REMOVE);
		#endif

		// render with the primary shader
		((PFNGLUSEPROGRAMPROC)wglGetProcAddress("glUseProgram"))(pidMain);
		#ifndef EDITOR_CONTROLS
			// if you don't have an audio system figure some other way to pass time to your shader
			#if USE_AUDIO

				// it is possible to upload your vars as vertex color attribute (gl_Color) to save one function import
				#if NO_UNIFORMS
					glColor3ui(MMTime.u.sample, 0, 0);
				#else
					// remember to divide your shader time variable with the SAMPLE_RATE (44100 with 4klang)
					//((PFNGLUNIFORM1IPROC)wglGetProcAddress("glUniform1i"))(0, MMTime.u.sample);
				#endif
			#endif
		#else
			position = track.getTime();
			((PFNGLUNIFORM1IPROC)wglGetProcAddress("glUniform1i"))(0, (static_cast<int>(position*44100.0)));

		#endif

		/******************
		* FFT
		*******************/
		LPVOID audio_ptr = NULL;
		DWORD audio_size = 0;

		// Read audio
		HRESULT hr = IDirectSoundBuffer_Lock(direct_sound_buffer, 0, FFT_SIZE * sizeof(SUsample), &audio_ptr, &audio_size, NULL, NULL, DSBLOCK_FROMWRITECURSOR);

		if (SUCCEEDED(hr) && audio_ptr) {
			if (playCursor < ((SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE) - (FFT_SIZE* SU_CHANNEL_COUNT * SU_SAMPLE_SIZE)))
			{
				SUsample* samples = (SUsample*)audio_ptr;
				for (int i = 0; i < FFT_SIZE; ++i) {
					fft_input[i] = (float)samples[i];
				}
			}

			IDirectSoundBuffer_Unlock(direct_sound_buffer, audio_ptr, audio_size, NULL, 0);
		}

		// Calculate FFT
		compute_fft(fft_input, fft_output);

		// Normalize output
		for (int i = 0; i < (FFT_SIZE / 4); i++)
		{	
			float gain = 50.0f;
			float alpha = 0.15f; // "Hidastaa" FFT:n piikkejä
			float threshhold = 0.05f; // Alin arvo mikä päästetään shaderille (vähentää "noisea")
			float x_t = fft_output[i] * gain;
			// Exponential smoothing kaava
			// s(t) = alpha*x(t)+(1-alpha)*s(t-1)
			fft_uniform[i] = (x_t < threshhold) ? 0.f : alpha*(x_t) + (1-alpha)*fft_uniform[i];
		}

		syncs[0] = (float)playCursor / (SU_SAMPLE_RATE * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE); // Aika sekunteina.

        for (int i = 0; i < SU_NUMSYNCS; ++i)
		{
			syncs[i + 1] = syncBuf[(playCursor / (2 * sizeof(SUsample)) >> 8) * SU_NUMSYNCS + i];
		}

        PFNGLUNIFORM1FVPROC glUniform1fvProc = ((PFNGLUNIFORM1FVPROC)wglGetProcAddress("glUniform1fv"));
		glUniform1fvProc(0, SU_NUMSYNCS + 1, syncs);
		glUniform1fvProc(8, FFT_SIZE / 4, fft_uniform);

		glRects(-1, -1, 1, 1);

		//syncs[0] = -syncs[0];
		//glUniform1fvProc(0, SU_NUMSYNCS + 1, syncs);

		// render "post process" using the opengl backbuffer
		#if POST_PASS
			glBindTexture(GL_TEXTURE_2D, 1);
			#if USE_MIPMAPS
				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
				glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, XRES, YRES, 0);
				((PFNGLGENERATEMIPMAPPROC)wglGetProcAddress("glGenerateMipmap"))(GL_TEXTURE_2D);
			#else
				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
				glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, XRES, YRES, 0);
			#endif
			((PFNGLACTIVETEXTUREPROC)wglGetProcAddress("glActiveTexture"))(GL_TEXTURE0);
			((PFNGLUSEPROGRAMPROC)wglGetProcAddress("glUseProgram"))(pidPost);
			((PFNGLUNIFORM1IPROC)wglGetProcAddress("glUniform1i"))(0, 0);
			glRects(-1, -1, 1, 1);
		#endif

		SwapBuffers(hDC);

		// handle functionality of the editor
		#ifdef EDITOR_CONTROLS
			editor.endFrame(timeGetTime());
			position = editor.handleEvents(&track, position);
			editor.printFrameStatistics();
			editor.updateShaders(&pidMain, &pidPost);
		#endif
		if (playCursor < lastPlayCursor)  {
			break;
		}
		lastPlayCursor = playCursor;
	} while(!GetAsyncKeyState(VK_ESCAPE));

	ExitProcess(0);
}
