
#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

#region ================== Namespaces

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.IO;
using System.Reflection;
using System.Linq;
using System.Diagnostics;
using CodeImp.DoomBuilder.Windows;
using CodeImp.DoomBuilder.IO;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.Rendering;
using CodeImp.DoomBuilder.Geometry;
using System.Drawing;
using CodeImp.DoomBuilder.Editing;
using CodeImp.DoomBuilder.Plugins;
using CodeImp.DoomBuilder.Actions;
using CodeImp.DoomBuilder.Types;
using CodeImp.DoomBuilder.Config;
using CodeImp.DoomBuilder.Data;
using CodeImp.DoomBuilder.BuilderModes;
// using CodeImp.DoomBuilder.GZBuilder.Geometry;
using CodeImp.DoomBuilder.VisualModes;

#endregion

namespace CodeImp.DoomBuilder.ThreeDFloorMode
{
	//
	// MANDATORY: The plug!
	// This is an important class to the Doom Builder core. Every plugin must
	// have exactly 1 class that inherits from Plug. When the plugin is loaded,
	// this class is instantiated and used to receive events from the core.
	// Make sure the class is public, because only public classes can be seen
	// by the core.
	//
	[Flags]
	public enum PlaneType
	{
		Floor = 1,
		Ceiling = 2,
        Bottom = 4, // Floor of 3D floor control sector
        Top = 8 // Ceiling of 3D floor control sector
	}

	public enum LabelDisplayOption
	{
		Always,
		Never,
		WhenHighlighted,
	}

	public class BuilderPlug : Plug
	{
		#region ================== Variables

		private bool additiveselect;
		private bool additivepaintselect;
		private float highlightrange;
		private bool autoclearselection;
		private MenusForm menusform;
		private bool usehighlight;
		private ControlSectorArea controlsectorarea;
        private float highlightsloperange;
		private List<SlopeVertexGroup> slopevertexgroups;
		private float stitchrange;
		private Sector slopedatasector;
		private bool updateafteraction;
		private string updateafteractionname;
		private LabelDisplayOption sectorlabeldisplayoption;
		private LabelDisplayOption slopevertexlabeldisplayoption;
		private PreferencesForm preferencesform;
		private bool selectchangedafterundoredo;

		// TMP
		public List<Line3D> drawlines;
		public List<Vector3D> drawpoints;

		#endregion

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

		public bool AdditiveSelect { get { return additiveselect; } }
		public bool AdditivePaintSelect { get { return additivepaintselect; } }
		public float HighlightRange { get { return highlightrange; } }
		public bool AutoClearSelection { get { return autoclearselection; } }
		public MenusForm MenusForm { get { return menusform; } }
		public bool UseHighlight
		{
			get
			{
				return usehighlight;
			}
			set
			{
				usehighlight = value;
				General.Map.Renderer3D.ShowSelection = usehighlight;
				General.Map.Renderer3D.ShowHighlight = usehighlight;
			}
		}
		public ControlSectorArea ControlSectorArea { get { return controlsectorarea; } }
        public float HighlightSlopeRange { get { return highlightsloperange; } }
		public List<SlopeVertexGroup> SlopeVertexGroups { get { return slopevertexgroups; } set { slopevertexgroups = value; } }
		public float StitchRange { get { return stitchrange; } }
		public bool SelectChangedAfterUndoRedo { get { return selectchangedafterundoredo; } internal set { selectchangedafterundoredo = value; } }

		public Sector SlopeDataSector { get { return slopedatasector; } set { slopedatasector = value; } }

		public LabelDisplayOption SectorLabelDisplayOption { get { return sectorlabeldisplayoption; } set { sectorlabeldisplayoption = value; } }
		public LabelDisplayOption SlopeVertexLabelDisplayOption { get { return slopevertexlabeldisplayoption; } set { slopevertexlabeldisplayoption = value; } }

		#endregion

 		// Static instance. We can't use a real static class, because BuilderPlug must
		// be instantiated by the core, so we keep a static reference. (this technique
		// should be familiar to object-oriented programmers)
		private static BuilderPlug me;

		// Lines tagged to the selected sectors
		private static ThreeDFloorEditorWindow tdfew = new ThreeDFloorEditorWindow();

		public static ThreeDFloorEditorWindow TDFEW { get { return tdfew; } }

		// Static property to access the BuilderPlug
		public static BuilderPlug Me { get { return me; } }

      	// This plugin relies on some functionality that wasn't there in older versions
		public override int MinimumRevision { get { return 1310; } }

		// This event is called when the plugin is initialized
		public override void OnInitialize()
		{
			base.OnInitialize();

			usehighlight = true;

			LoadSettings();

			slopevertexgroups = new List<SlopeVertexGroup>();

			//controlsectorarea = new ControlSectorArea(-512, 0, 512, 0, -128, -64, 128, 64, 64, 56);

			// This binds the methods in this class that have the BeginAction
			// and EndAction attributes with their actions. Without this, the
			// attributes are useless. Note that in classes derived from EditMode
			// this is not needed, because they are bound automatically when the
			// editing mode is engaged.
            General.Actions.BindMethods(this);

			menusform = new MenusForm();

  			// TODO: Add DB2 version check so that old DB2 versions won't crash
			// General.ErrorLogger.Add(ErrorType.Error, "zomg!");

			// Keep a static reference
            me = this;

			// TMP
			drawlines = new List<Line3D>();
			drawpoints = new List<Vector3D>();

		}

		// This is called when the plugin is terminated
		public override void Dispose()
		{
			base.Dispose();

			// This must be called to remove bound methods for actions.
            General.Actions.UnbindMethods(this);
        }

		public override void OnMapNewEnd()
		{
			base.OnMapNewEnd();

			controlsectorarea = new ControlSectorArea(-512, 0, 512, 0, 64, 56);
			BuilderPlug.Me.ControlSectorArea.LoadConfig();

			slopevertexgroups.Clear();

			slopedatasector = null;
		}

		public override void OnMapOpenEnd()
		{
			base.OnMapOpenEnd();

			controlsectorarea = new ControlSectorArea(-512, 0, 512, 0, 64, 56);
			BuilderPlug.Me.ControlSectorArea.LoadConfig();

			// Try to find the slope data sector and store slope information in it
			slopedatasector = GetSlopeDataSector();

			if (slopedatasector != null)
				LoadSlopeVertexGroupsFromSector();
		}

		public override void OnUndoEnd()
		{
			base.OnUndoEnd();

			// Load slope vertex data from the dummy sector
			LoadSlopeVertexGroupsFromSector();
		}

		public override void OnRedoEnd()
		{
			base.OnRedoEnd();

			// Load slope vertex data from the dummy sector
			LoadSlopeVertexGroupsFromSector();
		}
	
		public override void OnActionBegin(CodeImp.DoomBuilder.Actions.Action action)
		{
			base.OnActionBegin(action);

			string[] monitoractions = {
				"buildermodes_raisesector8", "buildermodes_lowersector8", "buildermodes_raisesector1",
				"buildermodes_lowersector1", "builder_visualedit", "builder_classicedit"
			};

			if (General.Editing.Mode is SlopeMode)
				return;

			if (monitoractions.Contains(action.Name))
			{
				updateafteraction = true;
				updateafteractionname = action.Name;
			}
			//else
			//	updateafteraction = false;
		}

		public override void OnActionEnd(CodeImp.DoomBuilder.Actions.Action action)
		{
			base.OnActionEnd(action);

			if (!updateafteraction && action.Name != updateafteractionname)
				return;

			updateafteraction = false;

			Dictionary<SlopeVertexGroup, int> updatesvgs = new Dictionary<SlopeVertexGroup, int>();

			// Find SVGs that needs to be updated, and change the SV z positions
			foreach (SlopeVertexGroup svg in slopevertexgroups)
			{
				bool update = false;
				Dictionary<int, List<Sector>> newheights = new Dictionary<int, List<Sector>>();

				foreach (Sector s in svg.Sectors)
				{
					if (s.Fields == null)
						continue;

					if ((svg.SectorPlanes[s] & PlaneType.Floor) == PlaneType.Floor)
					{
						if (s.Fields.ContainsKey("user_floorplane_id") && s.Fields.GetValue("user_floorplane_id", -1) == svg.Id)
						{
							if (svg.Height != s.FloorHeight)
							{
								int diff = s.FloorHeight - svg.Height;

								if (!newheights.ContainsKey(diff))
									newheights.Add(diff, new List<Sector>() { s });
								else
									newheights[diff].Add(s);

								update = true;
								//break;
							}
						}
					}
					
					if ((svg.SectorPlanes[s] & PlaneType.Ceiling) == PlaneType.Ceiling)
					{
						if (s.Fields.ContainsKey("user_ceilingplane_id") && s.Fields.GetValue("user_ceilingplane_id", -1) == svg.Id)
						{
							if (svg.Height != s.CeilHeight)
							{
								int diff = s.CeilHeight - svg.Height;

								if (!newheights.ContainsKey(diff))
									newheights.Add(diff, new List<Sector>() { s });
								else
									newheights[diff].Add(s);

								update = true;
								//break;
							}
						}
					}
				}

				// Debug.WriteLine(String.Format("floordiff: {0} / ceilingdiff: {1} / height: {2}", floordiff, ceilingdiff, svg.Height));

				if (update)
				{
					if (newheights.Count > 1)
						Debug.WriteLine(String.Format("Slope: multiple new heights, doing nothing. Your map is fucked!"));
					else if (!updatesvgs.ContainsKey(svg))
							updatesvgs.Add(svg, newheights.First().Key);
				}
			}

			// Update the slopes, and also update the view if in visual mode
			foreach (SlopeVertexGroup svg in updatesvgs.Keys)
			{
				foreach (SlopeVertex sv in svg.Vertices)
					sv.Z += updatesvgs[svg];

				svg.ComputeHeight();

				foreach (Sector s in svg.Sectors)
					UpdateSlopes(s);

				// Save the updated data in the sector
				svg.StoreInSector(slopedatasector);

				if (General.Editing.Mode is BaseVisualMode)
				{
					List<Sector> sectors = new List<Sector>();
					List<VisualSector> visualsectors = new List<VisualSector>();
					BaseVisualMode mode = ((BaseVisualMode)General.Editing.Mode);

					foreach (Sector s in svg.Sectors)
					{
						sectors.Add(s);

						// Get neighbouring sectors and add them to the list
						foreach (Sidedef sd in s.Sidedefs)
						{
							if (sd.Other != null && !sectors.Contains(sd.Other.Sector))
								sectors.Add(sd.Other.Sector);
						}
					}

					foreach (Sector s in svg.TaggedSectors)
					{
						if(!sectors.Contains(s))
							sectors.Add(s);

						// Get neighbouring sectors and add them to the list
						foreach (Sidedef sd in s.Sidedefs)
						{
							if (sd.Other != null && !sectors.Contains(sd.Other.Sector))
								sectors.Add(sd.Other.Sector);
						}
					}

					foreach (Sector s in sectors)
						visualsectors.Add(mode.GetVisualSector(s));

					foreach (VisualSector vs in visualsectors) vs.UpdateSectorGeometry(true);
					foreach (VisualSector vs in visualsectors) vs.UpdateSectorData();
				}
			}
		}

		public override bool OnModeChange(EditMode oldmode, EditMode newmode)
		{
			if (newmode != null && oldmode != null)
			{
				if (newmode.GetType().Name == "DragSectorsMode")
				{
					foreach (SlopeVertexGroup svg in slopevertexgroups)
						if (svg.Reposition)
							svg.GetAnchor();
				}
				else if(oldmode.GetType().Name == "DragSectorsMode")
				{
					foreach (SlopeVertexGroup svg in slopevertexgroups)
						if (svg.Reposition)
						{
							svg.RepositionByAnchor();
							svg.StoreInSector(slopedatasector);
						}
				}

			}

			return base.OnModeChange(oldmode, newmode);
		}

		// When the Preferences dialog is shown
		public override void OnShowPreferences(PreferencesController controller)
		{
			base.OnShowPreferences(controller);

			// Load preferences
			preferencesform = new PreferencesForm();
			preferencesform.Setup(controller);
		}

		// When the Preferences dialog is closed
		public override void OnClosePreferences(PreferencesController controller)
		{
			base.OnClosePreferences(controller);

			// Apply settings that could have been changed
			LoadSettings();

			// Unload preferences
			preferencesform.Dispose();
			preferencesform = null;
		}

		#region ================== Actions

		#endregion

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

		private Sector GetSlopeDataSector()
		{
			foreach (Sector s in General.Map.Map.Sectors)
			{
				if (s.Fields.GetValue("user_slopedatasector", false) == true)
					return s;
			}

			return null;
		}

		public DialogResult ThreeDFloorEditor()
		{
			List<Sector> selectedSectors = new List<Sector>(General.Map.Map.GetSelectedSectors(true));

			if (selectedSectors.Count <= 0 && General.Editing.Mode.HighlightedObject is Sector)
				selectedSectors.Add((Sector)General.Editing.Mode.HighlightedObject);

			if (tdfew == null)
				tdfew = new ThreeDFloorEditorWindow();

			tdfew.ThreeDFloors = GetThreeDFloors(selectedSectors);
			DialogResult result = tdfew.ShowDialog((Form)General.Interface);

			return result;
		}

		// Use the same settings as the BuilderModes plugin
		private void LoadSettings()
		{
			additiveselect = General.Settings.ReadPluginSetting("BuilderModes", "additiveselect", false);
			additivepaintselect = General.Settings.ReadPluginSetting("BuilderModes", "additivepaintselect", false);
			highlightrange = General.Settings.ReadPluginSetting("BuilderModes", "highlightrange", 20);
			autoclearselection = General.Settings.ReadPluginSetting("BuilderModes", "autoclearselection", false);
			highlightsloperange = (float)General.Settings.ReadPluginSetting("BuilderModes", "highlightthingsrange", 10);
			stitchrange = (float)General.Settings.ReadPluginSetting("BuilderModes", "stitchrange", 20);
			slopevertexlabeldisplayoption = (LabelDisplayOption)General.Settings.ReadPluginSetting("slopevertexlabeldisplayoption", (int)LabelDisplayOption.Always);
			sectorlabeldisplayoption = (LabelDisplayOption)General.Settings.ReadPluginSetting("sectorlabeldisplayoption", (int)LabelDisplayOption.Always);
			selectchangedafterundoredo = General.Settings.ReadPluginSetting("BuilderModes", "selectchangedafterundoredo", false);
		}

		public void StoreSlopeVertexGroupsInSector()
		{
			if (slopedatasector != null && !slopedatasector.IsDisposed)
			{
				slopedatasector.Fields.BeforeFieldsChange();

				if (!slopedatasector.Fields.ContainsKey("user_slopedatasector"))
				{
					slopedatasector.Fields.Add("user_slopedatasector", new UniValue(UniversalType.Boolean, true));
				}

				slopedatasector.Fields["comment"] = new UniValue(UniversalType.String, "[!]DO NOT EDIT OR DELETE! This sector is used by the slope mode for undo/redo operations.");

				foreach (SlopeVertexGroup svg in slopevertexgroups)
					svg.StoreInSector(slopedatasector);
			}
		}

		public void LoadSlopeVertexGroupsFromSector()
		{
			Regex svgregex = new Regex(@"user_svg(\d+)_v0_x", RegexOptions.IgnoreCase);

			slopevertexgroups.Clear();

			if (slopedatasector == null || slopedatasector.IsDisposed)
				return;

			foreach (KeyValuePair<string, UniValue> kvp in slopedatasector.Fields)
			{
				Match svgmatch = svgregex.Match((string)kvp.Key);

				if (svgmatch.Success)
				{
					int svgid = Convert.ToInt32(svgmatch.Groups[1].ToString());

					slopevertexgroups.Add(new SlopeVertexGroup(svgid, slopedatasector));
				}
			}

			General.Map.Map.Update();
		}

		public void UpdateSlopes()
		{
			// foreach (Sector s in General.Map.Map.Sectors)
			//	UpdateSlopes(s);

			foreach (SlopeVertexGroup svg in slopevertexgroups)
			{
				foreach (Sector s in svg.Sectors)
				{
					if (s != null && !s.IsDisposed)
						UpdateSlopes(s);
				}
			}
		}

		public Vector3D CRS(Vector3D p0, Vector3D p1, Vector3D p2, Vector3D p3, float t)
		{
			return 0.5f * ((2 * p1) +
				(-p0 + p2) * t +
				(2 * p0 - 5 * p1 + 4 * p2 - p3) * (t * t) +
				(-p0 + 3 * p1 - 3 * p2 + p3) * (t * t * t)
			);
		}

		public void UpdateSlopes(Sector s)
		{
			string[] fieldnames = new string[] { "user_floorplane_id", "user_ceilingplane_id" };

			foreach (string fn in fieldnames)
			{
				int id = s.Fields.GetValue(fn, -1);

				if (id == -1)
				{
					if (fn == "user_floorplane_id")
					{
						s.FloorSlope = new Vector3D();
						s.FloorSlopeOffset = 0;
					}
					else
					{
						s.CeilSlope = new Vector3D();
						s.CeilSlopeOffset = 0;
					}

					continue;
				}

				List<Vector3D> sp = new List<Vector3D>();
				SlopeVertexGroup svg = GetSlopeVertexGroup(id);

				// If the SVG does not exist unbind the SVG info from this sector
				if (svg == null)
				{
					s.Fields.Remove(fn);
					continue;
				}

				if (svg.Spline)
				{
					Vector2D center = new Vector2D(s.BBox.Width / 2 + s.BBox.X, s.BBox.Height / 2 + s.BBox.Y);
					List<Line3D> splinelines = new List<Line3D>();
					List<Vector3D> tangents = new List<Vector3D>();

					sp.Add(new Vector3D(svg.Vertices[1].Pos.x, svg.Vertices[1].Pos.y, svg.Vertices[1].Z));
					sp.Add(new Vector3D(svg.Vertices[0].Pos.x, svg.Vertices[0].Pos.y, svg.Vertices[0].Z));
					sp.Add(new Vector3D(svg.Vertices[2].Pos.x, svg.Vertices[2].Pos.y, svg.Vertices[2].Z));

					sp.Add(new Vector3D(sp[2].x-96, sp[2].y, sp[2].z-64));
					sp.Insert(0, new Vector3D(sp[0].x+96, sp[0].y, sp[0].z-64));
					//sp.Add(new Vector3D(sp[2] + (sp[2] - sp[1])));
					//sp.Insert(0, new Vector3D(sp[0] + (sp[0] - sp[1])));


					// Create tangents
					tangents.Add(new Vector3D());

					for (int i = 1; i <= 3; i++)
						tangents.Add(new Vector3D((sp[i + 1] - sp[i - 1]) / 2.0f));

					tangents.Add(new Vector3D());

					Debug.Print("----- tangents -----");
					for (int i = 0; i < tangents.Count; i++)
						Debug.Print(tangents[i].ToString());

					for(float u=0.0f; u < 1.0f; u += 0.1f)
					{
						splinelines.Add(new Line3D(
							CRS(sp[0], sp[1], sp[2], sp[3], u),
							CRS(sp[0], sp[1], sp[2], sp[3], u+0.1f)
						));
						/*
						splinelines.Add(new Line3D(
								Tools.HermiteSpline(sp[1], tangents[1], sp[2], tangents[2], u),
								Tools.HermiteSpline(sp[1], tangents[1], sp[2], tangents[2], u+0.1f)
							)
						);
						*/
					}

					for (float u = 0.0f; u < 1.0f; u += 0.1f)
					{
					splinelines.Add(new Line3D(
						CRS(sp[1], sp[2], sp[3], sp[4], u),
						CRS(sp[1], sp[2], sp[3], sp[4], u + 0.1f)
					));
					/*
					splinelines.Add(new Line3D(
							Tools.HermiteSpline(sp[2], tangents[2], sp[3], tangents[3], u),
							Tools.HermiteSpline(sp[2], tangents[2], sp[3], tangents[3], u + 0.1f)
						)
					);
					*/
					}

					drawlines.Clear();
					drawlines.AddRange(splinelines);

					drawpoints.Clear();
					drawpoints.AddRange(sp);

					Line2D sl1 = new Line2D(sp[1], sp[2]);
					Line2D sl2 = new Line2D(sp[2], sp[3]);
					List<Vector3D> points = new List<Vector3D>();

					Debug.Print("----- spline lines -----");
					foreach(Line3D l in splinelines)
						Debug.Print(l.Start.ToString() + " / " + l.End.ToString());

					foreach (Sidedef sd in s.Sidedefs)
					{
						double u = 0.0f;
						Plane ldplane = new Plane(sd.Line.Start.Position, sd.Line.End.Position, new Vector3D(sd.Line.Start.Position.x, sd.Line.Start.Position.y, 128), true);

						foreach(Line3D l in splinelines)
						{
							if(ldplane.GetIntersection(l.Start, l.End, ref u))
							{
								if (u < 0.0f || u > 1.0f)
									continue;

								Vector3D v = (l.End - l.Start) * u + l.Start;
								points.Add(v);
							}
						}

						/*
						if(sd.Line.Line.GetIntersection(sl1, out u))
							points.Add(Tools.HermiteSpline(sp[1], tangents[1], sp[2], tangents[2], u));

						if (sd.Line.Line.GetIntersection(sl2, out u))
							points.Add(Tools.HermiteSpline(sp[2], tangents[2], sp[3], tangents[3], u));
						*/
					}

					
					if (fn == "user_floorplane_id")
					{
						/*
						s.FloorSlope = new Vector3D(p.a, p.b, p.c);
						s.FloorSlopeOffset = p.d;
						*/
					}
					else
					{
						List<Vector3D> ps = new List<Vector3D>();

						if (points.Count > 2)
							points.RemoveAt(0);

						Vector2D perp = new Line2D(points[0], points[1]).GetPerpendicular();

						ps.Add(points[0]);
						ps.Add(points[1]);
						ps.Add(new Vector3D(points[0].x+perp.x, points[0].y+perp.y, points[0].z));

						Debug.Print("----- points -----");
						for (int i = 0; i < ps.Count; i++)
							Debug.Print(ps[i].ToString());

						/*
						for(int i=0; i < ps.Count; i++)
						{
							ps[i] = new Vector3D(ps[i].x, ps[i].z, ps[i].y);
						}
						*/

						Debug.Print("-----");
						for (int i = 0; i < ps.Count; i++)
							Debug.Print(ps[i].ToString());

						Plane p = new Plane(ps[0], ps[1], ps[2], false);

						s.CeilSlope = new Vector3D(p.a, p.b, p.c);
						s.CeilSlopeOffset = p.d;
					}

				}
				else // No spline
				{
					for (int i = 0; i < svg.Vertices.Count; i++)
					{
						sp.Add(new Vector3D(svg.Vertices[i].Pos.x, svg.Vertices[i].Pos.y, svg.Vertices[i].Z));
					}

					if (svg.Vertices.Count == 2)
					{
						double z = sp[0].z;
						Line2D line = new Line2D(sp[0], sp[1]);
						Vector3D perpendicular = line.GetPerpendicular();

						Vector2D v = sp[0] + perpendicular;

						sp.Add(new Vector3D(v.x, v.y, z));
					}

					if (fn == "user_floorplane_id")
					{
						Plane p = new Plane(sp[0], sp[1], sp[2], true);

						s.FloorSlope = new Vector3D(p.a, p.b, p.c);
						s.FloorSlopeOffset = p.d;
						s.FloorHeight = svg.Height;
						svg.Height = s.FloorHeight;
					}
					else
					{
						Plane p = new Plane(sp[0], sp[1], sp[2], false);

						s.CeilSlope = new Vector3D(p.a, p.b, p.c);
						s.CeilSlopeOffset = p.d;
						s.CeilHeight = svg.Height;
						svg.Height = s.CeilHeight;
					}
				}
			}
		}

		public static List<ThreeDFloor> GetThreeDFloors(List<Sector> sectors)
		{
			List<ThreeDFloor> tdf = new List<ThreeDFloor>();
			HashSet<Sector> tmpsectors = new HashSet<Sector>();
			HashSet<Sector> potentialsectors = new HashSet<Sector>();
			Dictionary<int, List<Sector>> tags = new Dictionary<int, List<Sector>>();

			// Immediately return if the list is empty
			if (sectors.Count == 0)
				return tdf;

			// Build a dictionary of tags used by 3D floor action and which control sector they belong to
			foreach (Linedef ld in General.Map.Map.Linedefs)
			{
				// Make sure the front sector is managed by the 3D floor plugin
				if (ld.Front == null || ld.Front.Sector == null || ld.Front.Sector.IsDisposed || (General.Map.UDMF && ld.Front.Sector.Fields.GetValue("user_managed_3d_floor", false) == false))
					continue;

				if ((ld.Action >= 100 && ld.Action < 300) && ld.Args[0] != 0)
				{
					if (!tags.ContainsKey(ld.Args[0]))
						tags.Add(ld.Args[0], new List<Sector>() { ld.Front.Sector });
					else
						tags[ld.Args[0]].Add(ld.Front.Sector);
				}
			}

			// Create a list of 3D floor control sectors that reference the given sectors
			foreach (Sector s in sectors)
			{
				if (s == null || s.IsDisposed)
					continue;

				IEnumerable<int> intersecttags = tags.Keys.Intersect(s.Tags);

				if (intersecttags.Count() == 0)
					continue;

				// This sector is tagged to contain a 3D floor. Using this will speed up creating the 3D floors later
				potentialsectors.Add(s);

				foreach(int it in intersecttags)
				{
					foreach (Sector its in tags[it])
						tmpsectors.Add(its);
				}
			}

			// Create 3D floors from the found sectors
			foreach(Sector s in tmpsectors)
				if(s != null)
					tdf.Add(new ThreeDFloor(s, potentialsectors));

			return tdf;
		}

		public static void ProcessThreeDFloors(List<ThreeDFloor> threedfloors)
		{
			ProcessThreeDFloors(threedfloors, null);
		}

		public static void ProcessThreeDFloors(List<ThreeDFloor> threedfloors, List<Sector> selectedSectors)
		{
			// List<Sector> selectedSectors = new List<Sector>(General.Map.Map.GetSelectedSectors(true));
			var sectorsByTag = new Dictionary<int, List<Sector>>();
			var sectorsToThreeDFloors = new Dictionary<Sector, List<ThreeDFloor>>();
			var sectorGroups = new List<List<Sector>>();
			List<int> tagblacklist = new List<int>();
			int numnewcontrolsectors = 0;

			if(selectedSectors == null)
				selectedSectors = new List<Sector>(General.Map.Map.GetSelectedSectors(true));

			var tmpSelectedSectors = new List<Sector>(selectedSectors);

			foreach (ThreeDFloor tdf in GetThreeDFloors(selectedSectors))
			{
				bool add = true;

				foreach (ThreeDFloor tdf2 in threedfloors)
				{
					if (tdf.Sector == tdf2.Sector)
					{
						add = false;
						break;
					}
				}

				if (add)
				{
					threedfloors.Add(tdf);
				}
			}

			tmpSelectedSectors = new List<Sector>(selectedSectors);

			General.Map.UndoRedo.CreateUndo("Modify 3D floors");

			foreach (ThreeDFloor tdf in threedfloors)
			{
				// Create a list of all tags used by the control sectors. This is necessary so that
				// tags that will be assigned to not yet existing geometry will not be used
				foreach (int tag in tdf.Tags)
					if (!tagblacklist.Contains(tag))
						tagblacklist.Add(tag);

				// Collect the number of control sectors that have to be created
				if (tdf.IsNew)
					numnewcontrolsectors++;
			}

			try
			{
				List<DrawnVertex> drawnvertices = new List<DrawnVertex>();

				if (numnewcontrolsectors > 0)
					drawnvertices = Me.ControlSectorArea.GetNewControlSectorVertices(numnewcontrolsectors);

				foreach (ThreeDFloor tdf in threedfloors)
				{
					if (tdf.IsNew)
						tdf.CreateGeometry(tagblacklist, drawnvertices);

					tdf.UpdateGeometry();
				}
			}
			catch (Exception e)
			{
				MessageBox.Show(e.Message + "\nPlease increase the size of the control sector area.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
				General.Map.UndoRedo.WithdrawUndo();
				return;
			}

			// Fill the sectorsToThreeDFloors dictionary, with a selected sector as key
			// and a list of all 3D floors, that should be applied to to this sector, as value
			foreach (Sector s in selectedSectors)
			{
				// Multiple tags is actually a UDMF field, so make sure it gets recorded for undo/redo
				s.Fields.BeforeFieldsChange();

				if (!sectorsToThreeDFloors.ContainsKey(s))
					sectorsToThreeDFloors.Add(s, new List<ThreeDFloor>());

				foreach (ThreeDFloor tdf in threedfloors)
				{
					if (tdf.TaggedSectors.Contains(s))
						sectorsToThreeDFloors[s].Add(tdf);
				}
			}

			// Group all selected sectors by their 3D floors. I.e. each element of sectorGroups
			// is a list of sectors that have the same 3D floors
			while (tmpSelectedSectors.Count > 0)
			{
				Sector s1 = tmpSelectedSectors.First();
				var list = new List<Sector>();
				var delsectors = new List<Sector>();

				foreach (Sector s2 in tmpSelectedSectors)
				{
					if (sectorsToThreeDFloors[s1].ContainsAllElements(sectorsToThreeDFloors[s2]))
					{
						list.Add(s2);
						delsectors.Add(s2);
					}
				}

				foreach (Sector s in delsectors)
					tmpSelectedSectors.Remove(s);

				tmpSelectedSectors.Remove(s1);

				sectorGroups.Add(list);
			}

			// Bind the 3D floors to the selected sectors
			foreach (List<Sector> sectors in sectorGroups)
			{
				if (General.Map.UDMF == true && General.Map.Config.SectorMultiTag)
				{
					foreach (Sector s in sectors)
					{
						// Remove all tags associated to 3D floors from the sector...
						foreach (ThreeDFloor tdf in threedfloors)
						{
							if (s.Tags.Contains(tdf.UDMFTag))
								s.Tags.Remove(tdf.UDMFTag);
						}

						// ... and re-add the ones that are still associated
						foreach (ThreeDFloor tdf in sectorsToThreeDFloors[s])
						{
							if (!s.Tags.Contains(tdf.UDMFTag))
								s.Tags.Add(tdf.UDMFTag);
						}

						// Remove tag 0 if there are other tags present, or add tag 0 if the sector has no tags
						if (s.Tags.Count > 1 && s.Tags.Contains(0))
							s.Tags.Remove(0);

						if(s.Tags.Count == 0)
							s.Tags.Add(0);

					}
				}
				else // No sector multi tagging support
				{
					int newtag;

					// Just use sectors.First(), all elements in sectors have the same 3D floors anyway
					// If there are no 3D floors associated set the tag to 0
					if (sectorsToThreeDFloors[sectors.First()].Count == 0)
						newtag = 0;
					else
						try
						{
							newtag = BuilderPlug.Me.ControlSectorArea.GetNewSectorTag(tagblacklist);
							tagblacklist.Add(newtag);
						}
						catch (Exception e)
						{
							MessageBox.Show(e.Message + "\nPlease increase the custom tag range.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
							General.Map.UndoRedo.WithdrawUndo();
							return;
						}


					foreach (Sector s in sectors)
						s.Tag = newtag;

					try
					{
						foreach (ThreeDFloor tdf in sectorsToThreeDFloors[sectors.First()])
							tdf.BindTag(newtag, null);
					}
					catch (Exception e)
					{
						MessageBox.Show(e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
						General.Map.UndoRedo.WithdrawUndo();
						return;
					}
				}
			}

			// Remove unused tags from the 3D floors
			foreach (ThreeDFloor tdf in threedfloors)
				tdf.Cleanup();

			// Snap to map format accuracy
			General.Map.Map.SnapAllToAccuracy();

			// Update textures
			General.Map.Data.UpdateUsedTextures();

			// Update caches
			General.Map.Map.Update();
			General.Interface.RedrawDisplay();
			General.Map.IsChanged = true;
		}

		public SlopeVertexGroup AddSlopeVertexGroup(List<SlopeVertex> vertices, out int id)
		{
			for (int i = 1; i < int.MaxValue; i++)
			{
				if (!slopevertexgroups.Exists(x => x.Id == i))
				{
					SlopeVertexGroup svg = new SlopeVertexGroup(i, (List<SlopeVertex>)vertices);
					
					slopevertexgroups.Add(svg);

					id = i;

					return svg;
				}
			}

			throw new Exception("No free slope vertex group ids");
		}

		public SlopeVertexGroup GetSlopeVertexGroup(SlopeVertex sv)
		{
			foreach (SlopeVertexGroup svg in slopevertexgroups)
			{
				if (svg.Vertices.Contains(sv))
					return svg;
			}

			return null;
		}

		public SlopeVertexGroup GetSlopeVertexGroup(int id)
		{
			foreach (SlopeVertexGroup svg in slopevertexgroups)
			{
				if (svg.Id == id)
					return svg;
			}

			return null;
		}

		public SlopeVertexGroup GetSlopeVertexGroup(Sector s)
		{
			foreach (SlopeVertexGroup svg in slopevertexgroups)
			{
				if (svg.Sectors.Contains(s))
					return svg;
			}

			return null;
		}

		public static List<Sector> GetSectorsByTag(int tag)
		{
			return GetSectorsByTag(General.Map.Map.Sectors, tag);
		}

		public static List<Sector> GetSectorsByTag(IEnumerable sectors, int tag)
		{
			List<Sector> taggedsectors = new List<Sector>();

			foreach (Sector s in sectors)
				if (s.Tags.Contains(tag))
					taggedsectors.Add(s);

			return taggedsectors;
		}

		#endregion
	}

	public static class ThreeDFloorHelpers
	{
		public static bool ContainsAllElements<T>(this List<T> list1, List<T> list2)
		{
			if (list1.Count != list2.Count)
				return false;

			foreach (T i in list1)
				if (!list2.Contains(i))
					return false;

			return true;
		}

		// Taken from http://stackoverflow.com/questions/10816803/finding-next-available-key-in-a-dictionary-or-related-collection
		// Add item to sortedList (numeric key) to next available key item, and return key
		public static int AddNext<T>(this SortedList<int, T> sortedList, T item)
		{
			int key = 1; // Make it 0 to start from Zero based index
			int count = sortedList.Count;

			int counter = 0;
			do
			{
				if (count == 0) break;
				int nextKeyInList = sortedList.Keys[counter++];

				if (key != nextKeyInList) break;

				key = nextKeyInList + 1;

				if (count == 1 || counter == count) break;


				if (key != sortedList.Keys[counter])
					break;

			} while (true);

			sortedList.Add(key, item);
			return key;
		}
	}
}
