#region ================== Copyright (c) 2014 Boris Iwanski

/*
 * Copyright (c) 2014 Boris Iwanski
 * This program is released under GNU General Public License
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 */

#endregion

using System;
using System.Windows.Forms;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Diagnostics;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.Rendering;
using CodeImp.DoomBuilder.Editing;
using CodeImp.DoomBuilder.Map;

namespace CodeImp.DoomBuilder.ThreeDFloorMode
{
	[Serializable]
	public class NoSpaceInCSAException : Exception
	{
		public NoSpaceInCSAException()
		{ }

		public NoSpaceInCSAException(string message) : base(message)
		{ }

		public NoSpaceInCSAException(string message, Exception innerException) : base(message, innerException)
		{ }
	}

	public class ControlSectorArea
	{

		#region ================== Enums

		public enum Highlight
		{
			None,
			OuterLeft,
			OuterRight,
			OuterTop,
			OuterBottom,
			OuterTopLeft,
			OuterTopRight,
			OuterBottomLeft,
			OuterBottomRight,
			Body
		};

		#endregion

		#region ================== Variables

		private RectangleF outerborder;
		private PixelColor bordercolor = new PixelColor(255, 0, 192, 0);
		private PixelColor fillcolor = new PixelColor(128, 0, 128, 0);
		private PixelColor borderhighlightcolor = new PixelColor(255, 0, 192, 0);
		private PixelColor fillhighlightcolor = new PixelColor(128, 0, 192, 0);
		private Dictionary<Highlight, Line2D> lines;
		private Dictionary<Highlight, Vector2D> points;
		private float gridsize;
		private float gridsizeinv;
		private float sectorsize;

		private double outerleft;
		private double outerright;
		private double outertop;
		private double outerbottom;

		private bool usecustomtagrange;
		private int firsttag;
		private int lasttag;

		#endregion

		#region ================== Properties

		public double GridSize { get { return gridsize; } }
		public double SectorSize { get { return sectorsize; } }

		public RectangleF OuterBorder { get { return outerborder; } }

		public double OuterLeft
		{
			get { return outerleft; }
			set { outerleft = value; UpdateLinesAndPoints();	}
		}

		public double OuterRight
		{
			get { return outerright; }
			set { outerright = value; UpdateLinesAndPoints(); }
		}

		public double OuterTop
		{
			get { return outertop; }
			set { outertop = value; UpdateLinesAndPoints(); }
		}

		public double OuterBottom
		{
			get { return outerbottom; }
			set { outerbottom = value; UpdateLinesAndPoints(); }
		}

		public bool UseCustomTagRnage { get { return usecustomtagrange; } set { usecustomtagrange = value; } }
		public int FirstTag { get { return firsttag; } set { firsttag = value; } }
		public int LastTag { get { return lasttag; } set { lasttag = value; } }

		#endregion

		#region ================== Constructor / Disposer

		public ControlSectorArea(float outerleft, float outerright, float outertop, float outerbottom, float gridsize, float sectorsize)
		{
			this.outerleft = outerleft;
			this.outerright = outerright;
			this.outertop = outertop;
			this.outerbottom = outerbottom;

			lines = new Dictionary<Highlight, Line2D>();
			points = new Dictionary<Highlight, Vector2D>();

			this.gridsize = gridsize;
			gridsizeinv = 1.0f / gridsize;

			this.sectorsize = sectorsize;

			UpdateLinesAndPoints();
		}

		#endregion

		#region ================== Methods

		public void UpdateLinesAndPoints()
		{
			lines[Highlight.OuterLeft] = new Line2D(outerleft, outertop, outerleft, outerbottom);
			lines[Highlight.OuterRight] = new Line2D(outerright, outertop, outerright, outerbottom);
			lines[Highlight.OuterTop] = new Line2D(outerleft, outertop, outerright, outertop);
			lines[Highlight.OuterBottom] = new Line2D(outerleft, outerbottom, outerright, outerbottom);

			points[Highlight.OuterTopLeft] = new Vector2D(outerleft, outertop);
			points[Highlight.OuterTopRight] = new Vector2D(outerright, outertop);
			points[Highlight.OuterBottomLeft] = new Vector2D(outerleft, outerbottom);
			points[Highlight.OuterBottomRight] = new Vector2D(outerright, outerbottom);

			outerborder = new RectangleF((float)outerleft, (float)outertop, (float)(outerright - outerleft), (float)(outerbottom - outertop));
		}

		public void Draw(IRenderer2D renderer, Highlight highlight)
		{
			PixelColor fcolor = highlight == Highlight.Body ? fillhighlightcolor : fillcolor;

			renderer.RenderRectangleFilled(
				new RectangleF((float)outerleft, (float)outertop, (float)(outerright - outerleft), (float)(outerbottom - outertop)),
				fcolor,
				true
			);

			// Draw the borders
			renderer.RenderRectangle(outerborder, 1.0f, bordercolor, true);

			// Highlight a border if necessary
			if (highlight >= Highlight.OuterLeft && highlight <= Highlight.OuterBottom)
				renderer.RenderLine(lines[highlight].v1, lines[highlight].v2, 1.0f, borderhighlightcolor, true);
			else
			{
				// Highlight the corners
				switch (highlight)
				{
					// Outer corners
					case Highlight.OuterTopLeft:
						renderer.RenderLine(lines[Highlight.OuterTop].v1, lines[Highlight.OuterTop].v2, 1.0f, borderhighlightcolor, true);
						renderer.RenderLine(lines[Highlight.OuterLeft].v1, lines[Highlight.OuterLeft].v2, 1.0f, borderhighlightcolor, true);
						break;
					case Highlight.OuterTopRight:
						renderer.RenderLine(lines[Highlight.OuterTop].v1, lines[Highlight.OuterTop].v2, 1.0f, borderhighlightcolor, true);
						renderer.RenderLine(lines[Highlight.OuterRight].v1, lines[Highlight.OuterRight].v2, 1.0f, borderhighlightcolor, true);
						break;
					case Highlight.OuterBottomLeft:
						renderer.RenderLine(lines[Highlight.OuterBottom].v1, lines[Highlight.OuterBottom].v2, 1.0f, borderhighlightcolor, true);
						renderer.RenderLine(lines[Highlight.OuterLeft].v1, lines[Highlight.OuterLeft].v2, 1.0f, borderhighlightcolor, true);
						break;
					case Highlight.OuterBottomRight:
						renderer.RenderLine(lines[Highlight.OuterBottom].v1, lines[Highlight.OuterBottom].v2, 1.0f, borderhighlightcolor, true);
						renderer.RenderLine(lines[Highlight.OuterRight].v1, lines[Highlight.OuterRight].v2, 1.0f, borderhighlightcolor, true);
						break;
				}
			}
		}

		public Highlight CheckHighlight(Vector2D pos, double scale)
		{
			double distance = double.MaxValue;
			double d;
			Highlight highlight = Highlight.None;

			// Find a line to highlight
			foreach (Highlight h in (Highlight[])Enum.GetValues(typeof(Highlight)))
			{
				if (h >= Highlight.OuterLeft && h <= Highlight.OuterBottom)
				{
					d = Line2D.GetDistanceToLine(lines[h].v1, lines[h].v2, pos, true);

					if (d <= BuilderModes.BuilderPlug.Me.HighlightRange / scale && d < distance)
					{
						distance = d;
						highlight = h;
					}
				}
			}

			distance = double.MaxValue;

			// Find a corner to highlight
			foreach (Highlight h in (Highlight[])Enum.GetValues(typeof(Highlight)))
			{
				if (h >= Highlight.OuterTopLeft && h <= Highlight.OuterBottomRight)
				{
					d = Vector2D.Distance(pos, points[h]);

					if (d <= BuilderModes.BuilderPlug.Me.HighlightRange / scale && d < distance)
					{
						distance = d;
						highlight = h;
					}
				}
			}

			if (highlight != Highlight.None)
				return highlight;

			if (OuterLeft < pos.x && OuterRight > pos.x && OuterTop > pos.y && OuterBottom < pos.y)
				return Highlight.Body;

			return Highlight.None;
		}

		public void SnapToGrid(Highlight highlight, Vector2D pos, Vector2D lastpos)
		{
			Vector2D newpos = GridSetup.SnappedToGrid(pos, gridsize, gridsizeinv);

			switch (highlight)
			{
				case Highlight.Body:
					Vector2D diff = GridSetup.SnappedToGrid(pos, gridsize, gridsizeinv) - GridSetup.SnappedToGrid(lastpos, gridsize, gridsizeinv);
					outerleft += diff.x;
					outerright += diff.x;
					outertop += diff.y;
					outerbottom += diff.y;
					break;

				// Outer border
				case Highlight.OuterLeft:
					if (newpos.x < outerright) outerleft = newpos.x;
					break;
				case Highlight.OuterRight:
					if(newpos.x > outerleft) outerright = newpos.x;
					break;
				case Highlight.OuterTop:
					if (newpos.y > outerbottom) outertop = newpos.y;
					break;
				case Highlight.OuterBottom:
					if (newpos.y < outertop) outerbottom = newpos.y;
					break;

				// Outer corners
				case Highlight.OuterTopLeft:
					if (newpos.x < outerright) outerleft = newpos.x;
					if (newpos.y > outerbottom) outertop = newpos.y;
					break;
				case Highlight.OuterTopRight:
					if (newpos.x > outerleft) outerright = newpos.x;
					if (newpos.y > outerbottom) outertop = newpos.y;
					break;
				case Highlight.OuterBottomLeft:
					if (newpos.x < outerright) outerleft = newpos.x;
					if (newpos.y < outertop) outerbottom = newpos.y;
					break;
				case Highlight.OuterBottomRight:
					if (newpos.x > outerleft) outerright = newpos.x;
					if (newpos.y < outertop) outerbottom = newpos.y;
					break;
			}

			UpdateLinesAndPoints();
		}

		public List<Vector2D> GetRelocatePositions(int numsectors)
		{
			List<Vector2D> positions = new List<Vector2D>();
			BlockMap<BlockEntry> blockmap = CreateBlockmap(true);
			int margin = (int)((gridsize - sectorsize) / 2);

			for (int x = (int)outerleft; x < (int)outerright; x += (int)gridsize)
			{
				for (int y = (int)outertop; y > (int)outerbottom; y -= (int)gridsize)
				{
					List<BlockEntry> blocks = blockmap.GetLineBlocks(
						new Vector2D(x + 1, y - 1),
						new Vector2D(x + gridsize - 1, y - gridsize + 1)
					);

					// The way our blockmap is built and queried we will always get exactly one block
					// Try the next position of the current one is occupied by another sector
					if (blocks[0].Sectors.Any(s => SectorInNewControlSectorSpace(x, y, s)))
						continue;

					positions.Add(new Vector2D(x + margin, y - margin));
					numsectors--;

					if (numsectors == 0)
						return positions;
				}
			}

			throw new NoSpaceInCSAException("Not enough space for control sector relocation");
		}

		public List<DrawnVertex> GetNewControlSectorVertices()
		{
			return GetNewControlSectorVertices(1);
		}

		public List<DrawnVertex> GetNewControlSectorVertices(int numsectors)
		{
			List<DrawnVertex> dv = new List<DrawnVertex>();
			BlockMap<BlockEntry> blockmap = CreateBlockmap();

			int margin = (int)((gridsize - sectorsize) / 2);

			// find position for new control sector
			for (int x = (int)outerleft; x < (int)outerright; x += (int)gridsize)
			{
				for (int y = (int)outertop; y > (int)outerbottom; y -= (int)gridsize)
				{
					List<BlockEntry> blocks = blockmap.GetLineBlocks(
						new Vector2D(x + 1, y - 1),
						new Vector2D(x + gridsize - 1, y - gridsize + 1)
					);

					// The way our blockmap is built and queried we will always get exactly one block
					// Try the next position of the current one is occupied by another sector
					if (blocks[0].Sectors.Any(s => SectorInNewControlSectorSpace(x, y, s)))
						continue;

					Point p = new Point(x + margin, y - margin);

					dv.Add(SectorVertex(p.X, p.Y));
					dv.Add(SectorVertex(p.X + BuilderPlug.Me.ControlSectorArea.SectorSize, p.Y));
					dv.Add(SectorVertex(p.X + BuilderPlug.Me.ControlSectorArea.SectorSize, p.Y - BuilderPlug.Me.ControlSectorArea.SectorSize));
					dv.Add(SectorVertex(p.X, p.Y - BuilderPlug.Me.ControlSectorArea.SectorSize));
					dv.Add(SectorVertex(p.X, p.Y));

					numsectors--;

					if (numsectors == 0)
						return dv;
				}
			}

			throw new NoSpaceInCSAException("No space left for control sectors");
		}

		/// <summary>
		/// Checks if the given sector and position for the new control sector intersect in any way.
		/// </summary>
		/// <param name="x">Leftmost X position of the new control sector space</param>
		/// <param name="y">Topmost Y position of the new control sector space</param>
		/// <param name="sector">The sector to check against</param>
		/// <returns>true if there's an intersection, false if there isn't</returns>
		private bool SectorInNewControlSectorSpace(int x, int y, Sector sector)
		{
			int margin = (int)((gridsize - sectorsize) / 2);
			HashSet<Vertex> sectorvertices = new HashSet<Vertex>();

			// Any of the sector's sidedef's linedef's vertices inside the new control sector's space?
			foreach (Sidedef sd in sector.Sidedefs)
			{
				sectorvertices.Add(sd.Line.Start);
				sectorvertices.Add(sd.Line.End);
			}

			foreach (Vertex v in sectorvertices)
				if (v.Position.x >= x + margin && v.Position.x <= x + margin + sectorsize && v.Position.y <= y - margin && v.Position.y >= y - margin - sectorsize)
					return true;

			// Any of the new vertex positions in the sector?
			Vector2D[] points = new Vector2D[]
			{
				new Vector2D(x + margin, y - margin),
				new Vector2D(x + margin + sectorsize, y - margin -sectorsize),
		 		new Vector2D(x + margin + sectorsize, y - margin - sectorsize),
				new Vector2D(x + margin, y -margin)
			};

			foreach (Vector2D v in points)
				if (sector.Intersect(v))
					return true;

			// Any of the new lines and the sector's lines overlapping?
			Line2D[] lines = new Line2D[]
			{
				new Line2D(points[0], points[1]),
				new Line2D(points[1], points[2]),
				new Line2D(points[2], points[3]),
				new Line2D(points[3], points[0])
			};

			foreach (Sidedef sd in sector.Sidedefs)
				foreach (Line2D line in lines)
					if (Line2D.GetIntersection(sd.Line.Line, line))
						return true;

			return false;
		}

		public bool Inside(float x, float y)
		{
			return Inside(new Vector2D(x, y));
		}

		public bool Inside(Vector2D pos)
		{
			if (pos.x > outerleft && pos.x < outerright && pos.y < outertop && pos.y > outerbottom)
				return true;

			return false;
		}

		public bool OutsideOuterBounds(float x, float y)
		{
			return OutsideOuterBounds(new Vector2D(x, y));
		}

		public bool OutsideOuterBounds(Vector2D pos)
		{
			if(pos.x < outerleft || pos.x > outerright || pos.y > outertop || pos.y < outerbottom)
				return true;

			return false;
		}

		// Aligns the area to the grid, expanding the area if necessary
		private RectangleF AlignAreaToGrid(RectangleF area)
		{
			List<float> f = new List<float>
			{
				area.Left,
				area.Top,
				area.Right,
				area.Bottom
			};

			for (int i = 0; i < f.Count; i++)
			{
				if (f[i] < 0)
					f[i] = (float)Math.Floor(f[i] / gridsize) * gridsize;
				else
					f[i] = (float)Math.Ceiling(f[i] / gridsize) * gridsize;
			}


			float l = f[0];
			float t = f[1];
			float r = f[2];
			float b = f[3];

			return new RectangleF(l, t, r - l, b - t);
		}

		private BlockMap<BlockEntry> CreateBlockmap()
		{
			return CreateBlockmap(false);
		}

		private BlockMap<BlockEntry> CreateBlockmap(bool ignorecontrolsectors)
		{
			// Make blockmap
			RectangleF area = MapSet.CreateArea(General.Map.Map.Vertices);
			area = MapSet.IncreaseArea(area, new Vector2D(outerleft, outertop));
			area = MapSet.IncreaseArea(area, new Vector2D(outerright, outerbottom));
			area = AlignAreaToGrid(area);

			BlockMap<BlockEntry> blockmap = new BlockMap<BlockEntry>(area, (int)gridsize);

			if (ignorecontrolsectors)
			{
				foreach (Sector s in General.Map.Map.Sectors)
				{
					// Managed control sectors have the custom UDMF field "user_managed_3d_floor" set to true
					// So if the field is NOT set, add the sector to the blockmap
					bool managed = s.Fields.GetValue("user_managed_3d_floor", false);

					if (managed == false)
						blockmap.AddSector(s);
					else // When a tag was manually removed a control sector still might have the user_managed_3d_floor field, but not be
					{    // recognized as a 3D floor control sector. In that case also add the sector to the blockmap
						bool orphaned = true;

						foreach(ThreeDFloor tdf in ((ThreeDFloorHelperMode)General.Editing.Mode).ThreeDFloors)
						{
							if(tdf.Sector == s)
							{
								orphaned = false;
								break;
							}
						}

						if (orphaned)
							blockmap.AddSector(s);
					}
				}
			}
			else
			{
				blockmap.AddSectorsSet(General.Map.Map.Sectors);
			}

			return blockmap;
		}

		public void Edit()
		{
			ControlSectorAreaConfig csacfg = new ControlSectorAreaConfig(this);

			csacfg.ShowDialog((Form)General.Interface);
		}

		// When OK is pressed on the preferences dialog
		// Prevent inlining, otherwise there are unexpected interactions with Assembly.GetCallingAssembly
		// See https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.getcallingassembly?view=netframework-4.6.1#remarks
		[MethodImplAttribute(MethodImplOptions.NoInlining)]
		public void SaveConfig()
		{
			ListDictionary config = new ListDictionary();

			config.Add("usecustomtagrange", usecustomtagrange);

			if (usecustomtagrange)
			{
				config.Add("firsttag", firsttag);
				config.Add("lasttag", lasttag);
			}

			config.Add("outerleft", outerleft);
			config.Add("outerright", outerright);
			config.Add("outertop", outertop);
			config.Add("outerbottom", outerbottom);

			General.Map.Options.WritePluginSetting("controlsectorarea", config);
		}

		// When OK is pressed on the preferences dialog
		// Prevent inlining, otherwise there are unexpected interactions with Assembly.GetCallingAssembly
		// See https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.getcallingassembly?view=netframework-4.6.1#remarks
		[MethodImplAttribute(MethodImplOptions.NoInlining)]
		public void LoadConfig()
		{
			ListDictionary config = (ListDictionary)General.Map.Options.ReadPluginSetting("controlsectorarea", new ListDictionary());

			usecustomtagrange = General.Map.Options.ReadPluginSetting("controlsectorarea.usecustomtagrange", false);
			firsttag = General.Map.Options.ReadPluginSetting("controlsectorarea.firsttag", 0);
			lasttag = General.Map.Options.ReadPluginSetting("controlsectorarea.lasttag", 0);

			outerleft = General.Map.Options.ReadPluginSetting("controlsectorarea.outerleft", outerleft);
			outerright = General.Map.Options.ReadPluginSetting("controlsectorarea.outerright", outerright);
			outertop = General.Map.Options.ReadPluginSetting("controlsectorarea.outertop", outertop);
			outerbottom = General.Map.Options.ReadPluginSetting("controlsectorarea.outerbottom", outerbottom);

			UpdateLinesAndPoints();
		}

		public int GetNewSectorTag(List<int> tagblacklist)
		{
			List<int> usedtags = new List<int>();

			if (usecustomtagrange)
			{
				for (int i = firsttag; i <= lasttag; i++)
				{
					if (!tagblacklist.Contains(i) && BuilderPlug.GetSectorsByTag(i).Count == 0)
						return i;
				}

				throw new Exception("No free tags in the custom range between " + firsttag.ToString() + " and " + lasttag.ToString() + ".");
			}

			return General.Map.Map.GetNewTag(tagblacklist);
		}

		public int GetNewLineID()
		{
			return General.Map.Map.GetNewTag();
		}

		// Turns a position into a DrawnVertex and returns it
		private DrawnVertex SectorVertex(double x, double y)
		{
			DrawnVertex v = new DrawnVertex();

			v.stitch = true;
			v.stitchline = true;
			v.pos = new Vector2D(Math.Round(x, General.Map.FormatInterface.VertexDecimals), Math.Round(y, General.Map.FormatInterface.VertexDecimals));

			return v;
		}

		private DrawnVertex SectorVertex(Vector2D v)
		{
			return SectorVertex(v.x, v.y);
		}

		static int GCD(int[] numbers)
		{
			return numbers.Aggregate(GCD);
		}

		static int GCD(int a, int b)
		{
			return b == 0 ? a : GCD(b, a % b);
		}

		#endregion
	}
}
