using System.Collections.Generic;
using OpenTK.Graphics.OpenGL;

namespace %%%PROJECT_ID%%%
{
	internal class GameWindow : OpenTK.GameWindow
	{
		private const int MOUSE_EVENT = 0;
		private const int KEY_EVENT = 1;
		private const int EXIT = 2;

		private const int MOUSE_LEFT = 0;
		private const int MOUSE_RIGHT = 1;
		private const int MOUSE_MOVE = 2;

		private const int DRAW_COMMAND_BLIT = 1;
		private const int DRAW_COMMAND_BLIT_PARTIAL = 2;
		private const int DRAW_COMMAND_RECTANGLE = 3;
		private const int DRAW_COMMAND_ELLIPSE = 4;
		private const int DRAW_COMMAND_LINE = 5;

		private Queue<int> events = new Queue<int>();
		private Queue<string> keyEvents = new Queue<string>();

		private int gameWidth;
		private int gameHeight;
		private int screenWidth;
		private int screenHeight;

		private static double fps = 60;
		private static GameWindow instance = null;
		public static GameWindow Instance { get { return instance; } }

		public static double FPS
		{
			get { return fps; }
			set { fps = value; }
		}

		private GameWindow(double fps, int gameWidth, int gameHeight, int screenWidth, int screenHeight)
			: base(screenWidth, screenHeight)
		{
			GameWindow.instance = this;
			this.gameWidth = gameWidth;
			this.gameHeight = gameHeight;
			this.screenWidth = screenWidth;
			this.screenHeight = screenHeight;
			this.X = 50;
			this.Y = 50;

			this.TargetRenderFrequency = fps;

			this.UpdateFrame += (sender, e) => this.Update();
			this.RenderFrame += (sender, e) => this.Render();
			this.Load += (sender, e) => this.Startup();
			this.Resize += (sender, e) => this.Resizing();

			this.Mouse.Move += (sender, e) => this.MouseMove(e.X, e.Y);
			this.Mouse.ButtonDown += (sender, e) => this.MouseButton(e.Button, e.X, e.Y, true);
			this.Mouse.ButtonUp += (sender, e) => this.MouseButton(e.Button, e.X, e.Y, false);
			this.Keyboard.KeyDown += (sender, e) => this.KeyEvent(e.Key, true);
			this.Keyboard.KeyUp += (sender, e) => this.KeyEvent(e.Key, false);
		}

		public static bool InitializeScreen(int gameWidth, int gameHeight)
		{
			return InitializeScreen(gameWidth, gameHeight, gameWidth, gameHeight);
		}

		public static bool InitializeScreen(int gameWidth, int gameHeight, int screenWidth, int screenHeight)
		{
			GameWindow gw = new GameWindow(fps, gameWidth, gameHeight, screenWidth, screenHeight);
			gw.Run(GameWindow.FPS, GameWindow.FPS);
			return false;
		}

		private void MouseMove(int x, int y)
		{
			events.Enqueue(MOUSE_EVENT);
			events.Enqueue(x * this.gameWidth / this.screenWidth);
			events.Enqueue(y * this.gameHeight / this.screenHeight);
			events.Enqueue(MOUSE_MOVE);
		}

		private void MouseButton(OpenTK.Input.MouseButton button, int x, int y, bool down)
		{
			// drop events that aren't the left or right buttons. For now.
			bool left = button == OpenTK.Input.MouseButton.Left;
			if (!left && button != OpenTK.Input.MouseButton.Right)
			{
				return;
			}

			events.Enqueue(MOUSE_EVENT);
			events.Enqueue(x * this.gameWidth / this.screenWidth);
			events.Enqueue(y * this.gameHeight / this.screenHeight);
			events.Enqueue(left ? MOUSE_LEFT : MOUSE_RIGHT);
			events.Enqueue(down ? 1 : 0);
		}

		private const int KEY_CODE_A = (int)OpenTK.Input.Key.A;
		private const int KEY_CODE_Z = (int)OpenTK.Input.Key.Z;
		private const int KEY_CODE_F1 = (int)OpenTK.Input.Key.F1;
		private const int KEY_CODE_F12 = (int)OpenTK.Input.Key.F12;
		private const int KEY_CODE_0 = (int)OpenTK.Input.Key.Number0;
		private const int KEY_CODE_9 = (int)OpenTK.Input.Key.Number9;

		private static readonly string[] LETTERS = "a b c d e f g h i j k l m n o p q r s t u v w x y z".Split(' ');
		private static readonly string[] F_KEYS = "f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12".Split(' ');
		private void KeyEvent(OpenTK.Input.Key key, bool down)
		{
			int keyCode = (int)key;

			if (keyCode >= KEY_CODE_A && keyCode <= KEY_CODE_Z)
			{
				keyEvents.Enqueue(LETTERS[keyCode - KEY_CODE_A]);
			}
			else if (keyCode >= KEY_CODE_F1 && keyCode <= KEY_CODE_F12)
			{
				keyEvents.Enqueue("f" + (1 + keyCode - KEY_CODE_F1));
			}
			else if (keyCode >= KEY_CODE_0 && keyCode <= KEY_CODE_9)
			{
				keyEvents.Enqueue("" + (keyCode - KEY_CODE_0));
			}
			else
			{
				string value;
				switch (key)
				{
					case OpenTK.Input.Key.Space: value = "space"; break;
					case OpenTK.Input.Key.Enter: value = "enter"; break;
					case OpenTK.Input.Key.KeypadEnter: value = "enter"; break;
					case OpenTK.Input.Key.Tab: value = "tab"; break;

					case OpenTK.Input.Key.Left: value = "left"; break;
					case OpenTK.Input.Key.Right: value = "right"; break;
					case OpenTK.Input.Key.Up: value = "up"; break;
					case OpenTK.Input.Key.Down: value = "down"; break;

					default: value = null; break;
				}

				if (value == null)
				{
					return;
				}
				keyEvents.Enqueue(value);
			}
			events.Enqueue(KEY_EVENT);
			events.Enqueue(down ? 1 : 0);
		}

		private static Dictionary<string, Value> STRING_CACHE = new Dictionary<string, Value>();
		private static Value CreateString(string s)
		{
			Value output;
			if (!STRING_CACHE.TryGetValue(s, out output))
			{
				output = new Value(%%%TYPE_STRING%%%, s);
				STRING_CACHE[s] = output;
			}
			return output;
		}

		private static Value CreateInteger(int n)
		{
			return CrayonWrapper.v_buildInteger(n);
		}

		private static Value CreateValueList(params Value[] values)
		{
			return new Value(%%%TYPE_LIST%%%, new List<Value>(values));
		}

		public List<Value> GetEvents()
		{
			List<Value> output = new List<Value>();
			Queue<int> events = instance.events;
			Queue<string> keyEvents = instance.keyEvents;
			int type;
			int x, y;
			bool down, isLeft;
			string keyCode;

			while (events.Count > 0)
			{
				switch (events.Dequeue())
				{
					case MOUSE_EVENT:
						x = events.Dequeue();
						y = events.Dequeue();
						type = events.Dequeue();
						if (type == MOUSE_MOVE)
						{
							output.Add(CrayonWrapper.v_buildGameEvent("mousemove", "mouse", x, y, 0, false, null));
						}
						else
						{
							isLeft = type == MOUSE_LEFT;
							down = events.Dequeue() == 1;
							string button = type == MOUSE_LEFT ? "left" : "right";
							output.Add(CrayonWrapper.v_buildGameEvent("mouse" + button + (down ? "down" : "up"), "mouse", x, y, 0, down, button));
						}
						break;
					case KEY_EVENT:
						down = events.Dequeue() == 1;
						keyCode = keyEvents.Dequeue();
						output.Add(CrayonWrapper.v_buildGameEvent("key" + (down ? "down" : "up"), "key", 0, 0, 0, down, keyCode));
						break;
					case EXIT:
						type = events.Dequeue();
						// TODO: more quit types.
						output.Add(CrayonWrapper.v_buildGameEvent("quit-x", "quit", 0, 0, 0, false, "x"));
						break;
					default:
						break;
				}
			}

			return output;
		}

		private void Startup()
		{
			GL.ClearColor(1f, 1f, 1f, 1f);
			GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);
			GL.Enable(EnableCap.Blend);
			GL.Disable(EnableCap.ColorMaterial);
		}

		private void Resizing()
		{
			this.screenWidth = this.Width;
			this.screenHeight = this.Height;

			GL.MatrixMode(MatrixMode.Projection);
			GL.LoadIdentity();
			GL.Ortho(0, this.screenWidth, this.screenHeight, 0, 10000, -10000);
			GL.Viewport(0, 0, this.screenWidth, this.screenHeight);
		}

		public void SetTitle(string value)
		{
			this.Title = value;
		}

		private void Update()
		{
			drawListVirtualLength = 0;
			imageListVirtualLength = 0;

			bool continueExecution = CrayonWrapper.v_runTick();

			if (!continueExecution)
			{
				// Because sometimes once isn't enough.
				this.Close();
				this.Exit();
				System.Environment.Exit(0);
			}
		}

		private void Render()
		{
			GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
			GL.ClearColor(0f, 0f, 0f, 1f);
			GL.MatrixMode(MatrixMode.Modelview);
			GL.LoadIdentity();

			this.PerformDraws(this.gameWidth, this.gameHeight, this.screenWidth, this.screenHeight);

			this.SwapBuffers();
		}

		private int[] drawList = new int[100];
		private int drawListVirtualLength = 0;
		private int drawListRealLength = 100;

		private Image[] imageList = new Image[100];
		private int imageListVirtualLength = 0;
		private int imageListRealLength = 100;

		private class RenderState
		{
			public BeginMode Mode;
			public int TextureID;
			public byte R;
			public byte G;
			public byte B;
			public byte A;
		}

		private void PerformDraws(int gameWidth, int gameHeight, int screenWidth, int screenHeight)
		{
			int RW = screenWidth;
			int RH = screenHeight;
			int VW = gameWidth;
			int VH = gameHeight;
			int left, top, right, bottom, startX, startY, endX, endY, lineWidth, width, height, scale;
			int textureId = -1;
			float textureWidth, textureHeight;
			float textureLeft, textureRight, textureTop, textureBottom;
			byte red, green, blue, alpha;
			Image image;

			RenderState state = null;

			int imageIndex = 0;
			for (int i = 0; i < drawListVirtualLength; i += 16)
			{
				switch (drawList[i])
				{
					case DRAW_COMMAND_BLIT:
						image = imageList[imageIndex++];
						left = drawList[i | 1];
						top = drawList[i | 2];
						startX = drawList[i | 3];
						startY = drawList[i | 4];
						width = drawList[i | 5];
						height = drawList[i | 6];
						scale = drawList[i | 7];
						right = left + width * scale;
						bottom = top + height * scale;
						if (right < 0 || left >= gameWidth || bottom < 0 || top >= gameHeight) continue;

						textureWidth = image.textureRight - image.textureLeft;
						textureHeight = image.textureBottom - image.textureTop;

						textureLeft = image.textureLeft + (textureWidth * startX / image.width);
						textureTop = image.textureTop + (textureHeight * startY / image.height);
						textureRight = textureLeft + (textureWidth * width / image.width);
						textureBottom = textureTop + (textureHeight * height / image.height);

						if (!image.compositeResource.loaded)
						{
							if (state != null)
							{
								state = null;
								GL.End();
								CrayonWrapper.v_loadCompositeImageResource(image.compositeResource);
							}
						}
						textureId = image.compositeResource.glTextureId;

						if (state == null ||
							state.Mode != BeginMode.Quads ||
							state.TextureID != textureId)
						{
							if (state == null)
							{
								state = new RenderState();
							}
							else
							{
								GL.End();
							}

							state.Mode = BeginMode.Quads;
							state.TextureID = textureId;

							GL.Enable(EnableCap.Texture2D);
							GL.Color4((byte)255, (byte)255, (byte)255, (byte)255);
							GL.BindTexture(TextureTarget.Texture2D, textureId);
							GL.Begin(BeginMode.Quads);
						}

						if (RW == VW && RH == VH)
						{
							GL.TexCoord2(textureLeft, textureTop);
							GL.Vertex2(left, top);

							GL.TexCoord2(textureRight, textureTop);
							GL.Vertex2(right, top);

							GL.TexCoord2(textureRight, textureBottom);
							GL.Vertex2(right, bottom);

							GL.TexCoord2(textureLeft, textureBottom);
							GL.Vertex2(left, bottom);
						}
						else
						{
							GL.TexCoord2(textureLeft, textureTop);
							GL.Vertex2(left * RW / VW, top * RH / VH);

							GL.TexCoord2(textureRight, textureTop);
							GL.Vertex2(right * RW / VW, top * RH / VH);

							GL.TexCoord2(textureRight, textureBottom);
							GL.Vertex2(right * RW / VW, bottom * RH / VH);

							GL.TexCoord2(textureLeft, textureBottom);
							GL.Vertex2(left * RW / VW, bottom * RH / VH);
						}
						break;

					case DRAW_COMMAND_RECTANGLE:
						left = drawList[i | 1];
						top = drawList[i | 2];
						right = drawList[i | 3] + left;
						bottom = drawList[i | 4] + top;
						red = (byte)drawList[i | 5];
						green = (byte)drawList[i | 6];
						blue = (byte)drawList[i | 7];
						alpha = (byte)drawList[i | 8];

						if (right < 0 || left >= gameWidth || bottom < 0 || top >= gameHeight) continue;

						if (state == null ||
							state.Mode != BeginMode.Quads ||
							state.TextureID != 0 ||
							state.R != red ||
							state.G != green ||
							state.B != blue ||
							state.A != alpha)
						{
							if (state == null)
							{
								state = new RenderState();
							}
							else
							{
								GL.End();
							}

							state.Mode = BeginMode.Quads;
							state.TextureID = 0;
							state.R = red;
							state.G = green;
							state.B = blue;
							state.A = alpha;

							GL.Disable(EnableCap.Texture2D);
							GL.Color4(red, green, blue, alpha);
							GL.Begin(BeginMode.Quads);
						}

						if (RW == VW && RH == VH)
						{
							GL.Vertex2(left, top);
							GL.Vertex2(right, top);
							GL.Vertex2(right, bottom);
							GL.Vertex2(left, bottom);
						}
						else
						{
							GL.Vertex2(left * RW / VW, top * RH / VH);
							GL.Vertex2(right * RW / VW, top * RH / VH);
							GL.Vertex2(right * RW / VW, bottom * RH / VH);
							GL.Vertex2(left * RW / VW, bottom * RH / VH);
						}
						break;

					case DRAW_COMMAND_ELLIPSE:
						left = drawList[i | 1];
						top = drawList[i | 2];
						right = drawList[i | 3] + left;
						bottom = drawList[i | 4] + top;
						red = (byte)drawList[i | 5];
						green = (byte)drawList[i | 6];
						blue = (byte)drawList[i | 7];
						alpha = (byte)drawList[i | 8];

						throw new System.NotImplementedException("TODO: draw ellipse");

					case DRAW_COMMAND_LINE:
						startX = drawList[i | 1];
						startY = drawList[i | 2];
						endX = drawList[i | 3];
						endY = drawList[i | 4];
						lineWidth = drawList[i | 5];
						red = (byte)drawList[i | 6];
						green = (byte)drawList[i | 7];
						blue = (byte)drawList[i | 8];
						alpha = (byte)drawList[i | 9];

						throw new System.NotImplementedException("TODO: draw line");

					default: throw new System.Exception("Unknown draw command");
				}
			}

			if (state != null)
			{
				GL.End();
			}
		}

		public void BlitImage(Image image, int x, int y)
		{
			if (drawListVirtualLength + 16 >= drawListRealLength)
			{
				drawList = DoubleArray<int>(drawList);
				drawListRealLength = drawList.Length;
			}

			if (imageListVirtualLength == imageListRealLength)
			{
				imageList = DoubleArray<Image>(imageList);
				imageListRealLength = imageList.Length;
			}

			if (!image.compositeResource.loaded)
			{
				image.compositeResource.glTextureId = GlUtil.ForceLoadTexture((System.Drawing.Bitmap)image.compositeResource.nativeBitmap);
				image.compositeResource.loaded = true;
			}

			drawList[drawListVirtualLength] = DRAW_COMMAND_BLIT;
			drawList[drawListVirtualLength | 1] = x;
			drawList[drawListVirtualLength | 2] = y;
			drawList[drawListVirtualLength | 3] = 0;
			drawList[drawListVirtualLength | 4] = 0;
			drawList[drawListVirtualLength | 5] = image.width;
			drawList[drawListVirtualLength | 6] = image.height;
			drawList[drawListVirtualLength | 7] = TranslationHelper.ProgramData.imageScaleStackTop;

			drawListVirtualLength += 16;

			imageList[imageListVirtualLength++] = image;
		}

		public void BlitImagePartial(Image image, int targetX, int targetY, int sourceX, int sourceY, int width, int height)
		{
			if (drawListVirtualLength + 16 >= drawListRealLength)
			{
				drawList = DoubleArray<int>(drawList);
				drawListRealLength = drawList.Length;
			}

			if (imageListVirtualLength == imageListRealLength)
			{
				imageList = DoubleArray<Image>(imageList);
				imageListRealLength = imageList.Length;
			}

			drawList[drawListVirtualLength] = DRAW_COMMAND_BLIT;
			drawList[drawListVirtualLength | 1] = targetX;
			drawList[drawListVirtualLength | 2] = targetY;
			drawList[drawListVirtualLength | 3] = sourceX;
			drawList[drawListVirtualLength | 4] = sourceY;
			drawList[drawListVirtualLength | 5] = width;
			drawList[drawListVirtualLength | 6] = height;
			drawList[drawListVirtualLength | 7] = TranslationHelper.ProgramData.imageScaleStackTop;

			drawListVirtualLength += 16;

			imageList[imageListVirtualLength++] = image;
		}

		public void DrawRectangle(int left, int top, int width, int height, int red, int green, int blue, int alpha)
		{
			if (drawListVirtualLength + 16 >= drawListRealLength)
			{
				drawList = DoubleArray<int>(drawList);
				drawListRealLength = drawList.Length;
			}

			drawList[drawListVirtualLength] = DRAW_COMMAND_RECTANGLE;
			drawList[drawListVirtualLength | 1] = left;
			drawList[drawListVirtualLength | 2] = top;
			drawList[drawListVirtualLength | 3] = width;
			drawList[drawListVirtualLength | 4] = height;
			drawList[drawListVirtualLength | 5] = red & 255;
			drawList[drawListVirtualLength | 6] = green & 255;
			drawList[drawListVirtualLength | 7] = blue & 255;
			drawList[drawListVirtualLength | 8] = alpha & 255;

			drawListVirtualLength += 16;
		}

		public void DrawEllipse(int left, int top, int width, int height, int red, int green, int blue, int alpha)
		{
			if (drawListVirtualLength + 16 >= drawListRealLength)
			{
				drawList = DoubleArray<int>(drawList);
				drawListRealLength = drawList.Length;
			}

			drawList[drawListVirtualLength] = DRAW_COMMAND_ELLIPSE;
			drawList[drawListVirtualLength | 1] = left;
			drawList[drawListVirtualLength | 2] = top;
			drawList[drawListVirtualLength | 3] = width;
			drawList[drawListVirtualLength | 4] = height;
			drawList[drawListVirtualLength | 5] = red & 255;
			drawList[drawListVirtualLength | 6] = green & 255;
			drawList[drawListVirtualLength | 7] = blue & 255;
			drawList[drawListVirtualLength | 8] = alpha & 255;
		}

		public void DrawLine(int startX, int startY, int endX, int endY, int lineWidth, int red, int green, int blue, int alpha)
		{
			if (drawListVirtualLength + 16 >= drawListRealLength)
			{
				drawList = DoubleArray<int>(drawList);
				drawListRealLength = drawList.Length;
			}

			drawList[drawListVirtualLength] = DRAW_COMMAND_LINE;
			drawList[drawListVirtualLength | 1] = startX;
			drawList[drawListVirtualLength | 2] = startY;
			drawList[drawListVirtualLength | 3] = endX;
			drawList[drawListVirtualLength | 4] = endY;
			drawList[drawListVirtualLength | 5] = lineWidth;
			drawList[drawListVirtualLength | 6] = red & 255;
			drawList[drawListVirtualLength | 7] = green & 255;
			drawList[drawListVirtualLength | 8] = blue & 255;
			drawList[drawListVirtualLength | 9] = alpha & 255;

			drawListVirtualLength += 16;
		}

		public void FillScreen(int red, int green, int blue)
		{
			drawListVirtualLength = 0;
			this.DrawRectangle(-1, -1, this.gameWidth + 2, this.gameHeight + 2, red, green, blue, 255);
		}

		private static T[] DoubleArray<T>(T[] original)
		{
			T[] newList = new T[original.Length * 2 + 1];
			System.Array.Copy(original, newList, original.Length);
			return newList;
		}
	}
}
