using SDL2;
using System;
using System.Diagnostics;
using ImGui;

namespace Strawberry.SDL2
{
	public class SDL2PlatformLayer : PlatformLayer
	{
		public int TransformMatrixLocation { get; private set; }
		public int TextureMatrixLocation { get; private set; }
		public Texture.Filters TextureFilter = .Nearest;
		public bool TextureClamping = false;
		public Color ClearColor = .Black;

		private SDL.Window* window;
		private SDL.Surface* screen;
		private SDL.Rect screenRect;
		private SDL.Renderer* renderer;
		private Shader shader;
		private SDL.SDL_GameController*[] gamepads;
		private bool* keyboard;

		private SDL.SDL_GLContext glContext;
		private uint glProgram;

		public this(String title, int screenWidth, int screenHeight, int windowScale)
			: base(title, screenWidth, screenHeight, windowScale)
		{
			SDL.Version version;
			SDL.GetVersion(out version);
			Calc.Log("SDL Version {0}.{1}.{2}", version.major, version.minor, version.patch);

			{
				SDL.InitFlag init = .Video | .Events | .Audio | .Timer;
				if (Input.GamepadLimit > 0)
					init |= .GameController;
	
				if (SDL.Init(init) != 0)
					Runtime.FatalError("Failed to initialize SDL");
			}

			SDL.EventState(.JoyAxisMotion, .Disable);
			SDL.EventState(.JoyBallMotion, .Disable);
			SDL.EventState(.JoyHatMotion, .Disable);
			SDL.EventState(.JoyButtonDown, .Disable);
			SDL.EventState(.JoyButtonUp, .Disable);
			SDL.EventState(.JoyDeviceAdded, .Disable);
			SDL.EventState(.JoyDeviceRemoved, .Disable);

			//Graphics
			{
				screenRect = SDL.Rect(0, 0, (int32)(ScreenWidth * WindowScale), (int32)(ScreenHeight * WindowScale));
				window = SDL.CreateWindow(Title, .Centered, .Centered, screenRect.w, screenRect.h, .Shown | .OpenGL);
				renderer = SDL.CreateRenderer(window, -1, .Accelerated);
				screen = SDL.GetWindowSurface(window);
				SDLImage.Init(.PNG | .JPG);
				SDLTTF.Init();

				glContext = SDL.GL_CreateContext(window);
				SDL.GL_SetSwapInterval(1);
				GL.Init(=> SdlGetProcAddress);

				//We need to activate this somehwere to make use of the alpha layer
				GL.glEnable(GL.GL_BLEND);
				GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
				
				shader = new Shader(String[2] (
					// vertex shader
					"""
					#version 330
					uniform mat4 u_matrix;
					layout(location=0) in vec2 a_position;
					layout(location=1) in vec2 a_tex;
					layout(location=2) in vec4 a_color;
					layout(location=3) in vec3 a_type;
					out vec2 v_tex;
					out vec4 v_col;
					out vec3 v_type;
					void main(void)
					{
						gl_Position = u_matrix * vec4(a_position.xy, 0, 1);
						v_tex = a_tex;
						v_col = a_color;
						v_type = a_type;
					}
					""",

					// fragment shader
					"""
					#version 330
					uniform sampler2D u_texture;
					in vec2 v_tex;
					in vec4 v_col;
					in vec3 v_type;
					out vec4 o_color;
					void main(void)
					{
						vec4 color = texture(u_texture, v_tex);
						o_color =
							v_type.x * color * v_col + 
							v_type.y * color.a * v_col + 
							v_type.z * v_col;
					}
					"""));

				glProgram = GL.glCreateProgram();
				GL.glAttachShader(glProgram, shader.vertexHandle);
				GL.glAttachShader(glProgram, shader.fragmentHandle);
				GL.glLinkProgram(glProgram);

				TransformMatrixLocation = GL.glGetUniformLocation(glProgram, "u_matrix");
				TextureMatrixLocation = GL.glGetUniformLocation(glProgram, "u_texture");

				int32 logLen = 0;
				char8[1024] log;
				GL.glGetProgramInfoLog(glProgram, 1024, &logLen, &log);
				if (logLen > 0)
					Calc.Log(&log, logLen);
			}

			//Audio
			{
				SDLMixer.OpenAudio(44100, SDLMixer.MIX_DEFAULT_FORMAT, 2, 4096);
			}
			
			//Input
			{
				keyboard = SDL.GetKeyboardState(null);
				gamepads = new SDL.SDL_GameController*[Input.GamepadLimit];
				for (let i < gamepads.Count)
					gamepads[i] = SDL.GameControllerOpen((int32)i);
			}

			//ImGui
			{
				ImGui.CHECKVERSION();
				ImGui.CreateContext();
				ImGui.StyleColorsDark();
				ImGuiImplSDL2.InitForOpenGL(window, &glContext);
				ImGuiImplOpenGL3.Init();
			}
		}

		public ~this()
		{
			delete gamepads;
			delete shader;

			ImGui.ImGuiImplOpenGL3.Shutdown();
			ImGui.ImGuiImplSDL2.Shutdown();
			ImGui.ImGui.DestroyContext();

			GL.glDeleteProgram(glProgram);
			SDL.GL_DeleteContext(glContext);
			SDL.DestroyWindow(window);
			SDL.Quit();
		}

		static void* SdlGetProcAddress(StringView string)
		{
			return SDL.SDL_GL_GetProcAddress(string.ToScopeCStr!());
		}

		public override bool Closed()
		{
			SDL.Event event;
			return (SDL.PollEvent(out event) != 0 && event.type == .Quit);
		}

		public override uint32 Ticks => SDL.GetTicks();

		public override void RenderBegin()
		{
			GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);
			GL.glClearColor(ClearColor.Rf, ClearColor.Gf, ClearColor.Bf, ClearColor.Af);
			GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		}

		public override void GameRenderBegin()
		{
			GL.glUseProgram(glProgram);
		}

		public override void GameRenderEnd()
		{
			GL.glUseProgram(0);
			GL.glFlush();
		}

		public override void ImGuiRenderBegin()
		{
			ImGuiImplOpenGL3.NewFrame();
			ImGuiImplSDL2.NewFrame(window);
			ImGui.NewFrame();
		}

		public override void ImGuiRenderEnd()
		{
			ImGui.Render();
			ImGuiImplOpenGL3.RenderDrawData(ImGui.ImGui.GetDrawData());
		}

		public override void RenderEnd()
		{
			SDL.GL_SwapWindow(window);
		}

		public override Batcher CreateBatcher()
		{
			return new SDL2Batcher(this);
		}

		public override Texture LoadTexture(String path)
		{
			let surface = SDLImage.Load(path);
			Debug.Assert(surface != null, "Could not load from path.");
			Debug.Assert(surface.format.bytesPerPixel == 4, "Surface format incorrect.");

			var tex = new Texture(surface.w, surface.h, (uint8*)surface.pixels, TextureFilter, TextureClamping);
			SDL.FreeSurface(surface);

			return tex;
		}

		public override void UpdateInput()
		{
			SDL.PumpEvents();
			SDL.GameControllerUpdate();
		}

		public override bool PollKey(Keys key)
		{
			Debug.Assert(keyboard != null, "Polling keyboard before initialized");

			return keyboard[(uint32)key];
		}

		public override bool CapsLock => SDL.GetModState() & .Caps > 0;
		public override bool NumLock => SDL.GetModState() & .Num > 0;

		public override bool PollGamepadButton(int gamepadID, Buttons button)
		{
			Debug.Assert(gamepads != null, "Polling gamepad before initialized");
			Debug.Assert(gamepadID >= 0 && gamepadID < gamepads.Count, "Gamepad index out of range (increase Game.gamepadLimit?)");

			return SDL.GameControllerGetButton(gamepads[gamepadID], (SDL.SDL_GameControllerButton)button) == 1;
		}

		public override float PollGamepadAxis(int gamepadID, Axes axis)
		{
			Debug.Assert(gamepads != null, "Polling gamepad before initialized");
			Debug.Assert(gamepadID >= 0 && gamepadID < gamepads.Count, "Gamepad index out of range (increase Game.gamepadLimit?)");

			let val = SDL.GameControllerGetAxis(gamepads[gamepadID], (SDL.SDL_GameControllerAxis)axis);
			if (val == 0)
				return 0;
			else if (val > 0)
				return val / 32767f;
			else
				return val / 32768f;
		}
	}
}
