using System.Runtime.CompilerServices;

namespace Eto.Test.Sections.Drawing
{
	[Section("Drawing", "Draw Loop")]
	public class DrawLoopSection : Scrollable
	{
		readonly Drawable drawable;
		readonly DirectDrawingRenderer renderer;
		readonly Panel content;
		bool useCreateGraphics;
		Action drawFrame;
		Status status = new Status();

		class Status
		{
			public bool Stop { get; set; }
		}
		ManualResetEvent mre = new ManualResetEvent(false);

		public DrawLoopSection()
		{
			drawable = new Drawable
			{
				Style = "direct",
				BackgroundColor = Colors.Black
			};
			drawable.Paint += (sender, e) =>
			{
				renderer.DrawFrame(e.Graphics, drawable.Size);
				Application.Instance.AsyncInvoke(() => mre.Set());
			};
			renderer = new DirectDrawingRenderer();

			var layout = new DynamicLayout { DefaultSpacing = new Size(5, 5), Padding = new Padding(10) };
			layout.AddSeparateRow(UseTexturesAndGradients(), UseTextCoordinates(), UseCreateGraphics(), NumberOfElements());
			layout.Add(content = new Panel { Content = drawable });
			this.Content = layout;
		}
		
		Control NumberOfElements()
		{
			var control = new Slider { MinValue = 2, MaxValue = 300 };
			control.ValueBinding.Bind(renderer, r => r.NumberOfElements);

			var amount = new Label();
			amount.TextBinding.Bind(renderer, Binding.Property((DirectDrawingRenderer r) => r.NumberOfElements).Convert(r => $"({r})"));
			return new TableLayout(new TableRow(new TableCell(control, true), amount));
		}

		void DrawLoop(object data)
		{
			var currentStatus = (Status)data;
			renderer.RestartFPS();
			while (!currentStatus.Stop)
			{
				mre.Reset();
				var draw = drawFrame;
				if (draw != null)
					Application.Instance.AsyncInvoke(draw);
				mre.WaitOne(1000);
			}
		}

		void SetMode()
		{
			drawFrame = null;
			status.Stop = true;
			if (useCreateGraphics)
			{
				renderer.EraseBoxes = true;
				if (!drawable.SupportsCreateGraphics)
				{
					content.BackgroundColor = Colors.Red;
					content.Content = new Label { Text = "This platform does not support Drawable.CreateGraphics", TextColor = Colors.White, VerticalAlignment = VerticalAlignment.Center, TextAlignment = TextAlignment.Center };
					return;
				}
				drawFrame = DrawWithCreateGraphics;
				using (var graphics = drawable.CreateGraphics())
					graphics.Clear();
			}
			else
			{
				renderer.EraseBoxes = false;
				drawFrame = () => { if (!status.Stop) drawable.Invalidate(); };
			}
			content.Content = drawable;
			status = new Status();
			Task.Run(() => DrawLoop(status));
		}

		protected override void OnUnLoad(EventArgs e)
		{
			status.Stop = true;
			base.OnUnLoad(e);
		}

		void DrawWithCreateGraphics()
		{
			if (status.Stop)
				return;
			using (var graphics = drawable.CreateGraphics())
			{
				renderer.DrawFrame(graphics, drawable.Size);
			}
		}

		Control UseTexturesAndGradients()
		{
			var control = new CheckBox
			{
				Text = "Use Textures && Gradients",
				Checked = renderer.UseTexturesAndGradients
			};
			control.CheckedChanged += (sender, e) =>
			{
				renderer.UseTexturesAndGradients = control.Checked ?? false;
				lock (renderer.Boxes)
				{
					renderer.Boxes.Clear();
					renderer.RestartFPS();
				}
				if (useCreateGraphics && drawable.SupportsCreateGraphics)
					using (var graphics = drawable.CreateGraphics())
						graphics.Clear(Brushes.Black);
			};
			return control;
		}

		Control UseCreateGraphics()
		{
			var control = new CheckBox
			{
				Text = "Use CreateGraphics",
				Checked = useCreateGraphics
			};
			control.CheckedChanged += (sender, e) =>
			{
				useCreateGraphics = control.Checked ?? false;
				renderer.RestartFPS();
				Application.Instance.AsyncInvoke(SetMode);
			};
			return control;
		}

		Control UseTextCoordinates()
		{
			var control = new CheckBox
			{
				Text = "Show Text Coordinates",
				Checked = renderer.ShowTextCoordinates
			};

			control.CheckedChanged += (sender, e) =>
			{
				renderer.ShowTextCoordinates = control.Checked ?? false;
				lock (renderer.Boxes)
				{
					renderer.Boxes.Clear();
					renderer.RestartFPS();
				}
				if (useCreateGraphics && drawable.SupportsCreateGraphics)
					using (var graphics = drawable.CreateGraphics())
						graphics.Clear(Brushes.Black);
			};
			return control;
		}

		protected override void OnLoadComplete(EventArgs e)
		{
			base.OnLoadComplete(e);
			SetMode();
		}

		protected override void Dispose(bool disposing)
		{
			if (disposing)
				status.Stop = true;
			base.Dispose(disposing);
		}
	}

	public class DirectDrawingRenderer : INotifyPropertyChanged
	{
		readonly Image texture;
		readonly Font font;
		readonly SolidBrush textBrush;
		readonly SolidBrush eraseBrush;
		int _NumberOfElements = 20;
		Size? _canvasSize;

		public readonly Stopwatch Watch = new Stopwatch();
		public int TotalFrames { get; set; }
		public long PreviousFrameStartTicks { get; set; }
		public readonly List<Box> Boxes = new List<Box>();

		public event PropertyChangedEventHandler PropertyChanged;


		public bool UseTexturesAndGradients { get; set; }
		public bool ShowTextCoordinates { get; set; }
		public bool EraseBoxes { get; set; }
		public int NumberOfElements
		{
			get => _NumberOfElements;
			set
			{
				_NumberOfElements = value;
				if (_canvasSize != null)
				{
					if (Boxes.Count > _NumberOfElements)
					{
						var diff = Boxes.Count - _NumberOfElements;
						Boxes.RemoveRange(Boxes.Count - diff - 1, diff);
					}
					if (Boxes.Count < _NumberOfElements)
					{
						InitializeBoxes(_canvasSize.Value, _NumberOfElements - Boxes.Count);
					}
				}
				OnPropertyChanged();
				RestartFPS();
			}
		}

		private void OnPropertyChanged([CallerMemberName] string name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));


		public DirectDrawingRenderer()
		{
			texture = TestIcons.Textures;
			font = SystemFonts.Default();
			textBrush = new SolidBrush(Colors.White);
			eraseBrush = new SolidBrush(Colors.Black);
		}

		public void RestartFPS()
		{
			Watch.Restart();
			TotalFrames = 0;
		}

		public class Box
		{
			static readonly Random random = new Random();
			SizeF increment;
			readonly Color color;
			readonly float rotation;
			float angle;
			readonly Action<Graphics> draw;
			readonly Action<Graphics> erase;
			readonly Brush fillBrush;
			RectangleF position;
			IMatrix transform;

			bool DisplayTextCoordinates;

			public SizeF Increment { get { return increment; } set { increment = value; } }

			static Color GetRandomColor(Random random)
			{
				return Color.FromArgb(random.Next(byte.MaxValue), random.Next(byte.MaxValue), random.Next(byte.MaxValue));
			}

			public Box(Size canvasSize, bool useTexturesAndGradients, bool ShowTextCoordinates, DirectDrawingRenderer renderer)
			{
				DisplayTextCoordinates = ShowTextCoordinates;
				var size = new SizeF(random.Next(50) + 50, random.Next(50) + 50);
				var location = new PointF(random.Next(canvasSize.Width - (int)size.Width), random.Next(canvasSize.Height - (int)size.Height));
				position = new RectangleF(location, size);
				increment = new SizeF(random.Next(3) + 1, random.Next(3) + 1);
				if (random.Next(2) == 1)
					increment.Width = -increment.Width;
				if (random.Next(2) == 1)
					increment.Height = -increment.Height;

				angle = random.Next(360);
				rotation = (random.Next(20) - 10f) / 4f;

				

				var rect = new RectangleF(size);
				color = GetRandomColor(random);
				var colorPen = new Pen(color);
				var blackPen = Pens.Black;
				var blackBrush = Brushes.Black;
				switch (random.Next(useTexturesAndGradients ? 5 : 2))
				{
					case 0:
						draw = g => g.DrawRectangle(colorPen, rect);
						erase = g => g.DrawRectangle(blackPen, rect);
						break;
					case 1:
						draw = g => g.DrawEllipse(colorPen, rect);
						erase = g => g.DrawEllipse(blackPen, rect);
						break;
					case 2:
						switch (random.Next(2))
						{
							case 0:
								fillBrush = new LinearGradientBrush(GetRandomColor(random), GetRandomColor(random), PointF.Empty, new PointF(size.Width, size.Height));
								break;
							case 1:
								fillBrush = new TextureBrush(renderer.texture)
								{
									Transform = Matrix.FromScale(size / 80)
								};
								break;
						}
						draw = g => g.FillEllipse(fillBrush, rect);
						erase = g => g.FillEllipse(blackBrush, rect);
						break;
					case 3:
						switch (random.Next(3))
						{
							case 0:
								fillBrush = new LinearGradientBrush(GetRandomColor(random), GetRandomColor(random), PointF.Empty, new PointF(size.Width, size.Height));
								break;
							case 1:
								fillBrush = new TextureBrush(renderer.texture)
								{
									Transform = Matrix.FromScale(size / 80)
								};
								break;
							case 2:
								fillBrush = new RadialGradientBrush(GetRandomColor(random), GetRandomColor(random), (PointF)size / 2, (PointF)size / 2, size);
								break;
						}
						draw = g => g.FillRectangle(fillBrush, rect);
						erase = g => g.FillRectangle(blackBrush, rect);
						break;
					case 4:
						var font = Fonts.Sans(random.Next(20) + 4);
						draw = g => g.DrawText(font, color, 0, 0, "Some Text");
						erase = g => g.DrawText(font, Colors.Black, 0, 0, "Some Text");
						break;
				}
			}

			public void Move(Size bounds)
			{
				position.Offset(increment);
				var center = position.Center;
				if (increment.Width > 0 && center.X >= bounds.Width)
					increment.Width = -increment.Width;
				else if (increment.Width < 0 && center.X < 0)
					increment.Width = -increment.Width;

				if (increment.Height > 0 && center.Y >= bounds.Height)
					increment.Height = -increment.Height;
				else if (increment.Height < 0 && center.Y < 0)
					increment.Height = -increment.Height;
				angle += rotation;

				transform = Matrix.FromTranslation(position.Location);
				transform.RotateAt(angle, position.Width / 2, position.Height / 2);
			}

			public void Erase(Graphics graphics)
			{
				if (transform != null)
				{
					graphics.SaveTransform();
					graphics.MultiplyTransform(transform);
					erase(graphics);
					graphics.RestoreTransform();
				}
			}

			private string Coordinates()
			{
				return string.Format("{0}x{1}", position.X, position.Y);
			}

			public void CoordianateDisplay(Graphics graphics, Font font, Brush textbrush)
			{
				if (DisplayTextCoordinates)
				{
					graphics.DrawText(font, textbrush, position.X, position.Y, Coordinates());
				}
			}

			public void CoordianateErase(Graphics graphics, Font font, Brush erasebrush)
			{
				if (DisplayTextCoordinates)
				{
					// we can't just draw the text again in black or we leave "vapor trails" on the screen!
					// so clobber the text area with a filled rectangle of erase color
					var info = Coordinates();
					var sizeinfo = graphics.MeasureString(font, info);
					var rec = new RectangleF(position.X, position.Y, sizeinfo.Width, sizeinfo.Height);					
					graphics.FillRectangle(erasebrush, rec);
				}
			}



			public void Draw(Graphics graphics)
			{
				graphics.SaveTransform();
				graphics.MultiplyTransform(transform);
				draw(graphics);
				graphics.RestoreTransform();
			}
		}

		void InitializeBoxes(Size canvasSize, int count)
		{
			for (int i = 0; i < count; i++)
				Boxes.Add(new Box(canvasSize, UseTexturesAndGradients, ShowTextCoordinates, this));
		}

		public void DrawFrame(Graphics graphics, Size canvasSize)
		{
			if (graphics == null)
				return;
			lock (Boxes)
			{
				_canvasSize = canvasSize;
				if (Boxes.Count == 0 && canvasSize.Width > 1 && canvasSize.Height > 1)
					InitializeBoxes(canvasSize, NumberOfElements);

				var fps = TotalFrames / Watch.Elapsed.TotalSeconds;
				// The frames per second as determined by the last frame. Measuring a single frame
				// must include EndDraw, since that is when the pipeline is flushed to the device.
				var frameTicks = Watch.ElapsedTicks - PreviousFrameStartTicks;
				var lastFrameFps = Stopwatch.Frequency / Math.Max(frameTicks, 1);
				PreviousFrameStartTicks = Watch.ElapsedTicks;

				var bounds = canvasSize;
				graphics.AntiAlias = false;
				foreach (var box in Boxes)
				{
					if (EraseBoxes)
					{
						box.CoordianateErase(graphics, font, eraseBrush);
						box.Erase(graphics);
					}
					box.Move(bounds);
					box.Draw(graphics);
					box.CoordianateDisplay(graphics, font, textBrush);
				}
				
				var fpsText = string.Format("Frames per second since start: {0:0.00}, last: {1:0.00}", fps, lastFrameFps);
				if (EraseBoxes)
					graphics.FillRectangle(Colors.Black, new RectangleF(graphics.MeasureString(font, fpsText)));
				graphics.DrawText(font, textBrush, 0, 0, fpsText);
				
				TotalFrames++;
			}
		}
	}
}
