using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using MoonWorks.Audio;
using MoonWorks.Graphics;

namespace MoonWorks.Video
{
	public unsafe class VideoPlayer : IDisposable
	{
		public Texture RenderTexture { get; private set; } = null;
		public VideoState State { get; private set; } = VideoState.Stopped;
		public bool Loop { get; set; }
		public float Volume {
			get => volume;
			set
			{
				volume = value;
				if (audioStream != null)
				{
					audioStream.Volume = value;
				}
			}
		}
		public float PlaybackSpeed { get; set; } = 1;

		private Video Video = null;

		private GraphicsDevice GraphicsDevice;
		private Texture yTexture = null;
		private Texture uTexture = null;
		private Texture vTexture = null;
		private Sampler LinearSampler;

		private void* yuvData = null;
		private int yuvDataLength = 0;

		private int currentFrame;

		private AudioDevice AudioDevice;
		private StreamingSoundTheora audioStream = null;
		private float volume = 1.0f;

		private Stopwatch timer;
		private double lastTimestamp;
		private double timeElapsed;

		private bool disposed;

		public VideoPlayer(GraphicsDevice graphicsDevice, AudioDevice audioDevice)
		{
			GraphicsDevice = graphicsDevice;
			if (GraphicsDevice.VideoPipeline == null)
			{
				throw new InvalidOperationException("Missing video shaders!");
			}

			AudioDevice = audioDevice;
			LinearSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp);

			timer = new Stopwatch();
		}

		public void Load(Video video)
		{
			if (Video != video)
			{
				Stop();

				if (RenderTexture == null)
				{
					RenderTexture = CreateRenderTexture(GraphicsDevice, video.Width, video.Height);
				}

				if (yTexture == null)
				{
					yTexture = CreateSubTexture(GraphicsDevice, video.Width, video.Height);
				}

				if (uTexture == null)
				{
					uTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
				}

				if (vTexture == null)
				{
					vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
				}

				if (video.Width != RenderTexture.Width || video.Height != RenderTexture.Height)
				{
					RenderTexture.Dispose();
					RenderTexture = CreateRenderTexture(GraphicsDevice, video.Width, video.Height);
				}

				if (video.Width != yTexture.Width || video.Height != yTexture.Height)
				{
					yTexture.Dispose();
					yTexture = CreateSubTexture(GraphicsDevice, video.Width, video.Height);
				}

				if (video.UVWidth != uTexture.Width || video.UVHeight != uTexture.Height)
				{
					uTexture.Dispose();
					uTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
				}

				if (video.UVWidth != vTexture.Width || video.UVHeight != vTexture.Height)
				{
					vTexture.Dispose();
					vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
				}

				var newDataLength = (
					(video.Width * video.Height) +
					(video.UVWidth * video.UVHeight * 2)
				);

				if (newDataLength != yuvDataLength)
				{
					yuvData = NativeMemory.Realloc(yuvData, (nuint) newDataLength);
					yuvDataLength = newDataLength;
				}

				Video = video;

				InitializeTheoraStream();
			}
		}

		public void Play()
		{
			if (Video == null) { return; }

			if (State == VideoState.Playing)
			{
				return;
			}

			timer.Start();

			if (audioStream != null)
			{
				audioStream.Play();
			}

			State = VideoState.Playing;
		}

		public void Pause()
		{
			if (Video == null) { return; }

			if (State != VideoState.Playing)
			{
				return;
			}

			timer.Stop();

			if (audioStream != null)
			{
				audioStream.Pause();
			}

			State = VideoState.Paused;
		}

		public void Stop()
		{
			if (Video == null) { return; }

			if (State == VideoState.Stopped)
			{
				return;
			}

			timer.Stop();
			timer.Reset();

			lastTimestamp = 0;
			timeElapsed = 0;

			DestroyAudioStream();

			Theorafile.tf_reset(Video.Handle);

			State = VideoState.Stopped;
		}

		public void Unload()
		{
			Stop();
			Video = null;
		}

		public void Update()
		{
			if (Video == null) { return; }

			if (audioStream != null)
			{
				audioStream.Update();
			}
		}

		public void Render()
		{
			if (Video == null || State == VideoState.Stopped)
			{
				return;
			}

			timeElapsed += (timer.Elapsed.TotalMilliseconds - lastTimestamp) * PlaybackSpeed;
			lastTimestamp = timer.Elapsed.TotalMilliseconds;

			int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond)));
			if (thisFrame > currentFrame)
			{
				if (Theorafile.tf_readvideo(
					Video.Handle,
					(IntPtr) yuvData,
					thisFrame - currentFrame
				) == 1 || currentFrame == -1)
				{
					UpdateRenderTexture();
				}

				currentFrame = thisFrame;
			}

			bool ended = Theorafile.tf_eos(Video.Handle) == 1;
			if (ended)
			{
				timer.Stop();
				timer.Reset();

				DestroyAudioStream();

				Theorafile.tf_reset(Video.Handle);

				if (Loop)
				{
					// Start over!
					InitializeTheoraStream();

					timer.Start();
				}
				else
				{
					State = VideoState.Stopped;
				}
			}
		}

		private void UpdateRenderTexture()
		{
			var commandBuffer = GraphicsDevice.AcquireCommandBuffer();

			commandBuffer.SetTextureDataYUV(
				yTexture,
				uTexture,
				vTexture,
				(IntPtr) yuvData,
				(uint) yuvDataLength
			);

			commandBuffer.BeginRenderPass(
				new ColorAttachmentInfo(RenderTexture, Color.Black)
			);

			commandBuffer.BindGraphicsPipeline(GraphicsDevice.VideoPipeline);
			commandBuffer.BindFragmentSamplers(
				new TextureSamplerBinding(yTexture, LinearSampler),
				new TextureSamplerBinding(uTexture, LinearSampler),
				new TextureSamplerBinding(vTexture, LinearSampler)
			);

			commandBuffer.DrawPrimitives(0, 1, 0, 0);

			commandBuffer.EndRenderPass();

			GraphicsDevice.Submit(commandBuffer);
		}

		private static Texture CreateRenderTexture(GraphicsDevice graphicsDevice, int width, int height)
		{
			return Texture.CreateTexture2D(
				graphicsDevice,
				(uint) width,
				(uint) height,
				TextureFormat.R8G8B8A8,
				TextureUsageFlags.ColorTarget | TextureUsageFlags.Sampler
			);
		}

		private static Texture CreateSubTexture(GraphicsDevice graphicsDevice, int width, int height)
		{
			return Texture.CreateTexture2D(
				graphicsDevice,
				(uint) width,
				(uint) height,
				TextureFormat.R8,
				TextureUsageFlags.Sampler
			);
		}

		private void InitializeTheoraStream()
		{
			// Grab the first video frame ASAP.
			while (Theorafile.tf_readvideo(Video.Handle, (IntPtr) yuvData, 1) == 0);

			// Grab the first bit of audio. We're trying to start the decoding ASAP.
			if (AudioDevice != null && Theorafile.tf_hasaudio(Video.Handle) == 1)
			{
				DestroyAudioStream();

				int channels, sampleRate;
				Theorafile.tf_audioinfo(Video.Handle, out channels, out sampleRate);

				audioStream = new StreamingSoundTheora(AudioDevice, Video.Handle, channels, (uint) sampleRate);
			}

			currentFrame = -1;
		}

		private void DestroyAudioStream()
		{
			if (audioStream != null)
			{
				audioStream.StopImmediate();
				audioStream.Dispose();
				audioStream = null;
			}
		}

		protected virtual void Dispose(bool disposing)
		{
			if (!disposed)
			{
				if (disposing)
				{
					// dispose managed state (managed objects)
					RenderTexture.Dispose();
					yTexture.Dispose();
					uTexture.Dispose();
					vTexture.Dispose();
				}

				// free unmanaged resources (unmanaged objects) and override finalizer
				NativeMemory.Free(yuvData);

				disposed = true;
			}
		}

		~VideoPlayer()
		{
		    // 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);
		}
	}
}
