using System;
using System.Collections.Generic;
using System.IO;
using RefreshCS;

namespace MoonWorks.Graphics
{
	public class GraphicsDevice : IDisposable
	{
		public IntPtr Handle { get; }
		public Backend Backend { get; }

		private uint windowFlags;
		public SDL2.SDL.SDL_WindowFlags WindowFlags => (SDL2.SDL.SDL_WindowFlags) windowFlags;

		// Built-in video pipeline
		internal GraphicsPipeline VideoPipeline { get; }

		public bool IsDisposed { get; private set; }

		private readonly HashSet<WeakReference<GraphicsResource>> resources = new HashSet<WeakReference<GraphicsResource>>();

		public GraphicsDevice(
			Backend preferredBackend,
			bool debugMode
		) {
			Backend = (Backend) Refresh.Refresh_SelectBackend((Refresh.Backend) preferredBackend, out windowFlags);

			if (Backend == Backend.Invalid)
			{
				throw new System.Exception("Could not set graphics backend!");
			}

			Handle = Refresh.Refresh_CreateDevice(
				Conversions.BoolToByte(debugMode)
			);

			// Check for optional video shaders
			string basePath = System.AppContext.BaseDirectory;
			string videoVertPath = Path.Combine(basePath, "video_fullscreen.vert.refresh");
			string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.frag.refresh");
			if (File.Exists(videoVertPath) && File.Exists(videoFragPath))
			{
				ShaderModule videoVertShader = new ShaderModule(this, videoVertPath);
				ShaderModule videoFragShader = new ShaderModule(this, videoFragPath);

				VideoPipeline = new GraphicsPipeline(
					this,
					new GraphicsPipelineCreateInfo
					{
						AttachmentInfo = new GraphicsPipelineAttachmentInfo(
							new ColorAttachmentDescription(
								TextureFormat.R8G8B8A8,
								ColorAttachmentBlendState.None
							)
						),
						DepthStencilState = DepthStencilState.Disable,
						VertexShaderInfo = GraphicsShaderInfo.Create(
							videoVertShader,
							"main",
							0
						),
						FragmentShaderInfo = GraphicsShaderInfo.Create(
							videoFragShader,
							"main",
							3
						),
						VertexInputState = VertexInputState.Empty,
						RasterizerState = RasterizerState.CCW_CullNone,
						PrimitiveType = PrimitiveType.TriangleList,
						MultisampleState = MultisampleState.None
					}
				);
			}
		}

		public bool ClaimWindow(Window window, PresentMode presentMode)
		{
			var success = Conversions.ByteToBool(
				Refresh.Refresh_ClaimWindow(
					Handle,
					window.Handle,
					(Refresh.PresentMode) presentMode
				)
			);

			if (success)
			{
				window.Claimed = true;
				window.SwapchainFormat = GetSwapchainFormat(window);
				if (window.SwapchainTexture == null)
				{
					window.SwapchainTexture = new Texture(this, IntPtr.Zero, window.SwapchainFormat, 0, 0);
				}
			}

			return success;
		}

		public void UnclaimWindow(Window window)
		{
			Refresh.Refresh_UnclaimWindow(
				Handle,
				window.Handle
			);
			window.Claimed = false;
		}

		public void SetPresentMode(Window window, PresentMode presentMode)
		{
			Refresh.Refresh_SetSwapchainPresentMode(
				Handle,
				window.Handle,
				(Refresh.PresentMode) presentMode
			);
		}

		public CommandBuffer AcquireCommandBuffer()
		{
			return new CommandBuffer(this, Refresh.Refresh_AcquireCommandBuffer(Handle));
		}

		public unsafe void Submit(CommandBuffer commandBuffer)
		{
			var commandBufferPtrs = stackalloc IntPtr[1];

			commandBufferPtrs[0] = commandBuffer.Handle;

			Refresh.Refresh_Submit(
				Handle,
				1,
				(IntPtr) commandBufferPtrs
			);
		}

		public unsafe void Submit(
			CommandBuffer commandBufferOne,
			CommandBuffer commandBufferTwo
		) {
			var commandBufferPtrs = stackalloc IntPtr[2];

			commandBufferPtrs[0] = commandBufferOne.Handle;
			commandBufferPtrs[1] = commandBufferTwo.Handle;

			Refresh.Refresh_Submit(
				Handle,
				2,
				(IntPtr) commandBufferPtrs
			);
		}

		public unsafe void Submit(
			CommandBuffer commandBufferOne,
			CommandBuffer commandBufferTwo,
			CommandBuffer commandBufferThree
		) {
			var commandBufferPtrs = stackalloc IntPtr[3];

			commandBufferPtrs[0] = commandBufferOne.Handle;
			commandBufferPtrs[1] = commandBufferTwo.Handle;
			commandBufferPtrs[2] = commandBufferThree.Handle;

			Refresh.Refresh_Submit(
				Handle,
				3,
				(IntPtr) commandBufferPtrs
			);
		}

		public unsafe void Submit(
			CommandBuffer commandBufferOne,
			CommandBuffer commandBufferTwo,
			CommandBuffer commandBufferThree,
			CommandBuffer commandBufferFour
		) {
			var commandBufferPtrs = stackalloc IntPtr[4];

			commandBufferPtrs[0] = commandBufferOne.Handle;
			commandBufferPtrs[1] = commandBufferTwo.Handle;
			commandBufferPtrs[2] = commandBufferThree.Handle;
			commandBufferPtrs[3] = commandBufferFour.Handle;

			Refresh.Refresh_Submit(
				Handle,
				4,
				(IntPtr) commandBufferPtrs
			);
		}

		public unsafe void Submit(params CommandBuffer[] commandBuffers)
		{
			var commandBufferPtrs = stackalloc IntPtr[commandBuffers.Length];

			for (var i = 0; i < commandBuffers.Length; i += 1)
			{
				commandBufferPtrs[i] = commandBuffers[i].Handle;
			}

			Refresh.Refresh_Submit(
				Handle,
				(uint) commandBuffers.Length,
				(IntPtr) commandBufferPtrs
			);
		}

		public void Wait()
		{
			Refresh.Refresh_Wait(Handle);
		}

		private TextureFormat GetSwapchainFormat(Window window)
		{
			return (TextureFormat) Refresh.Refresh_GetSwapchainFormat(Handle, window.Handle);
		}

		internal void AddResourceReference(WeakReference<GraphicsResource> resourceReference)
		{
			lock (resources)
			{
				resources.Add(resourceReference);
			}
		}

		internal void RemoveResourceReference(WeakReference<GraphicsResource> resourceReference)
		{
			lock (resources)
			{
				resources.Remove(resourceReference);
			}
		}

		protected virtual void Dispose(bool disposing)
		{
			if (!IsDisposed)
			{
				if (disposing)
				{
					lock (resources)
					{
						foreach (var weakReference in resources)
						{
							if (weakReference.TryGetTarget(out var target))
							{
								target.Dispose();
							}
						}
						resources.Clear();
					}

					Refresh.Refresh_DestroyDevice(Handle);
				}

				IsDisposed = true;
			}
		}

		~GraphicsDevice()
		{
			// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
			Dispose(disposing: false);
		}

		public void Dispose()
		{
			// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
			Dispose(disposing: true);
			GC.SuppressFinalize(this);
		}
	}
}
