// A lovely 3D map editor
// wot I wrote.
//
// Go to "Window->Tilemap Editor" and it should appear.


using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;

public class TilemapEditor : EditorWindow
{
	//-------------------------------------------------------
	enum Mode {
		Select, Draw, Pick
	}
	enum Command {
		Fill, Erase, Cut, Copy, RotL, RotR, Up, Down,
	}
	static string[] modeNames = { "Select (S)", "Draw (D)", "Pick (Ctrl)" };
	static string[] cmdNames = { "Fill (F)", "Erase (E)", "Cut (X)", "Copy (C)",
		"Rotate Left (Shift+Z)", "Rotate Right (Z)", "Plane Up (Q)", "Plane Down (A)" };

	//-------------------------------------------------------
	// Stores a brushfull of tiles.
	public class Brush
	{
		public Vector3Int size;
		public GameObject[,,] prefabs;
		public Quaternion[,,] dirs;
		public Vector3Int hotspot;

		public Brush(Vector3Int _size) {
			size = _size;
			prefabs = new GameObject[size.z, size.y, size.x];
			dirs = new Quaternion[size.z, size.y, size.x];
			hotspot = Vector3Int.zero;
		}
	}

	// Helper to show a brush by tracking temporary scene objects.
	class BrushDisplay {
		TilemapEditor owner;
		Brush source;
		GameObject[,,] previews;
		Vector3Int pos, size, psize;

		public BrushDisplay(TilemapEditor _owner) { 
			owner = _owner;
		}

		public void SetBrush(Brush _source) {
			source = _source;
			Remove();
		}

		// Moves the display to a new position.
		public void MoveTo(Vector3Int pos) {
			if (source != null)
				MoveTo(pos, pos+source.size-Vector3Int.one);
		}

		// Repeats the display over a region.
		public void MoveTo(Vector3Int min, Vector3Int max) {
			Vector3Int _pos = min;
			Vector3Int _size = max - min + Vector3Int.one;

			if (previews == null || _size != size) {
				// Size changed, do a full rebuild.
				Remove();
				if (source != null) {
					psize = _size;
					previews = owner.StampBrush(source, min, max, true);
				}
			} else if (_pos != pos) {
				// Position only changed, just move them all along.
				GameObject top = owner.GetContainer();
				for (int z = 0; z<psize.z; z++) {
					for (int y = 0; y<psize.y; y++) {
						for (int x = 0; x<psize.x; x++) {
							GameObject obj = previews[z, y, x];
							if (obj != null)
								owner.PlaceTransform(top, obj.transform, 
									_pos + new Vector3Int(x, y, z) - source.hotspot, obj.transform.localRotation);
						}
					}
				}
			}

			pos = _pos;
			size = _size;
		}

		// Removes all evidence from the scene.
		public void Remove() {
			if (previews != null) {
				for (int z = 0; z<psize.z; z++) {
					for (int y = 0; y<psize.y; y++) {
						for (int x = 0; x<psize.x; x++) {
							GameObject obj = previews[z, y, x];
							if (obj != null)
								Object.DestroyImmediate(obj);
						}
					}
				}
				previews = null;
			}
		}
	}

	//-------------------------------------------------------

	// Grid->world mapping. Doesn't relocate the tiles once placed though.
	public float gridSize = 1.0f;

	// Tile configuration.
	public int tilesPerRow = 3;
	public string tileSetPath = "Assets";
	public GameObject[] tileSet;

	// Tool mode.
	Mode mode;

	// Current 2D editing plane.
	public int yPlane = 0;

	// The thing we're placing.
	Brush brushSource;

	// Currently hovered tile.
	bool hasCursor;
	Vector3Int cursor;

	// Area we're currently dragging out, valid while our UI is hot.
	Vector3Int dragStart, dragEnd;

	// Currently selected area (inclusive)
	bool hasSelection;
	Vector3Int selMin, selMax;
	Vector3 selDrag;

	// Misc.
	BrushDisplay brushDisplay;
	TilemapEditorData data;
	bool needsRepaint;
	Vector2 tileScrollPos;
	Font headerFont;
	Material highlight;
	Regex numberSplitter;
	const string tilemapName = "Tilemap";

	[MenuItem("Window/Tilemap Editor")]
	static void Initialize() {
		TilemapEditor win = EditorWindow.GetWindow<TilemapEditor>(false, "Tilemap Editor");
		win.titleContent.text = "Map Editor";
	}

	// Called when created.
	void OnEnable() {
		brushSource = null;
		brushDisplay = new BrushDisplay(this);

		highlight = new Material(Shader.Find("Unlit/TileHighlight")) {
			hideFlags = HideFlags.HideAndDontSave,
			color = new Color(0.0f, 0.2f, 1.0f)
		};

		numberSplitter = new Regex(@"^\d+\s+-\s+(.*)$", RegexOptions.Compiled);

		string guid = AssetDatabase.FindAssets("TilemapIcons")[0];
		data = AssetDatabase.LoadAssetAtPath<TilemapEditorData>(AssetDatabase.GUIDToAssetPath(guid));
		ReloadTiles();
	}

	void OnDisable() {
		RemoveBrush();
	}

	// Whenever our panel is open, hook into the scene UI.
	void OnBecameVisible() {
		SceneView.duringSceneGui -= OnSceneGUI;
		SceneView.duringSceneGui += OnSceneGUI;
		hasCursor = false;
	}

	void OnBecameInvisible() {
		SceneView.duringSceneGui -= OnSceneGUI;
		RemoveBrush();
	}

	void SetFlags(GameObject g) {
		if (PrefabUtility.GetPrefabAssetType(g) == PrefabAssetType.Model)
			g.isStatic = true;
	}

	// Finds all tiles in the asset path.
	public void ReloadTiles() {
		List<GameObject> results = new List<GameObject>();

		tileSetPath.TrimEnd('/');

		string[] guids = AssetDatabase.FindAssets("t:GameObject", new[] { tileSetPath });
		foreach (string guid in guids) {
			string path = AssetDatabase.GUIDToAssetPath(guid);
			Object obj = AssetDatabase.LoadAssetAtPath<GameObject>(path);
			if (obj != null)
				results.Add((GameObject)obj);
		}

		tileSet = results.ToArray();
		if (brushSource == null && tileSet.Length > 0)
			SetBrush(tileSet[0]);
	}

	void PlaceTransform(GameObject top, Transform t, Vector3Int pos, Quaternion facing) {
		Vector3 p = (Vector3)pos*gridSize + new Vector3(0.5f, 0.0f, 0.5f);
		t.position = top.transform.TransformPoint(p);
		t.rotation = top.transform.rotation * facing;
	}

	Vector3Int CellAt(Vector3 pos) {
		return new Vector3Int(
			Mathf.FloorToInt(pos.x / gridSize),
			Mathf.FloorToInt(pos.y / gridSize),
			Mathf.FloorToInt(pos.z / gridSize));
	}

	static void CalculateBounds(Vector3Int a, Vector3Int b, out Vector3Int min, out Vector3Int max) {
		min = Vector3Int.Min(a, b);
		max = Vector3Int.Max(a, b);
	}

	public void SetBrush(Brush from) {
		RemoveBrush();
		brushSource = from;
		brushDisplay.SetBrush(from);
		Repaint();
	}

	public void SetBrush(GameObject from) {
		Brush brush = new Brush(Vector3Int.one);
		brush.prefabs[0, 0, 0] = from;
		brush.dirs[0, 0, 0] = Quaternion.identity;
		SetBrush(brush);
	}

	void RemoveBrush() {
		brushDisplay.Remove();
	}

	static Vector3Int Rotate(Vector3Int pos, Vector3Int ssize) {
		return new Vector3Int(pos.z, pos.y, ssize.x-1-pos.x);
	}

	public void RotateBrush(int count) {
		if (brushSource != null) {
			// Keep rotating right one step until we get there.
			for (int i=0;i<count;i++) {
				Vector3Int ssize = brushSource.size;
				Quaternion rot = Quaternion.Euler(0, 90, 0);

				// Make a 90-degree rotated copy.
				Brush dest = new Brush(new Vector3Int(ssize.z, ssize.y, ssize.x));
				for (int z = 0; z<ssize.z; z++) {
					for (int y = 0; y<ssize.y; y++) {
						for (int x = 0; x<ssize.x; x++) {
							Vector3Int d = Rotate(new Vector3Int(x, y, z), ssize);
							dest.prefabs[d.z, d.y, d.x] = brushSource.prefabs[z, y, x];
							dest.dirs[d.z, d.y, d.x] = rot * brushSource.dirs[z, y, x];
						}
					}
				}

				dest.hotspot = Rotate(brushSource.hotspot, ssize);
				brushSource = dest;
			}

			brushDisplay.SetBrush(brushSource);
		}
	}

	public GameObject[] FindTiles(Vector3Int min, Vector3Int max) {
		GameObject top = GetContainer();

		List<GameObject> list = new List<GameObject>();
		foreach (Transform t in top.transform) {
			Vector3Int pos = CellAt(t.localPosition);
			if (pos.x >= min.x && pos.x <= max.x
				&& pos.y >= min.y && pos.y <= max.y
				&& pos.z >= min.z && pos.z <= max.z) {
				list.Add(t.gameObject);
			}
		}
		return list.ToArray();
	}

	static void ReplaceMaterial(GameObject g, Material newMat) {
		foreach (Renderer r in g.GetComponentsInChildren<Renderer>()) {
			var mats = new Material[r.sharedMaterials.Length];
			for (var j = 0; j < r.sharedMaterials.Length; j++)
				mats[j] = newMat;
			r.sharedMaterials = mats;
		}
	}

	// Stamps a brush into the scene.
	// The brush will be repeated over the entire length of the region.
	GameObject[,,] StampBrush(Brush brush, Vector3Int min, Vector3Int max, bool temporary = false) {
		if (!temporary)
			EraseRegion(min - brush.hotspot, max - brush.hotspot);

		GameObject top = GetContainer();

		Vector3Int dsize = max - min + Vector3Int.one;
		Vector3Int ssize = brush.size;
		GameObject[,,] list = new GameObject[dsize.z, dsize.y, dsize.x];

		for (int z = 0; z<dsize.z; z++) {
			for (int y = 0; y<dsize.y; y++) {
				for (int x = 0; x<dsize.x; x++) {
					// Repeat the source brush over the entire length.
					int sx = x % ssize.x;
					int sy = y % ssize.y;
					int sz = z % ssize.z;
					GameObject obj = brush.prefabs[sz, sy, sx];
					Quaternion dir = brush.dirs[sz, sy, sx];

					if (obj != null) {
						GameObject g = PrefabUtility.InstantiatePrefab(obj) as GameObject;
						list[z, y, x] = g;

						// Set the worldspace transform.
						PlaceTransform(top, g.transform, min + new Vector3Int(x, y, z) - brush.hotspot, dir);
						g.transform.localScale = new Vector3(
							g.transform.localScale.x * top.transform.localScale.x,
							g.transform.localScale.y * top.transform.localScale.y,
							g.transform.localScale.z * top.transform.localScale.z);

						if (temporary) {
							g.hideFlags = HideFlags.HideAndDontSave;
							g.isStatic = false;
							ReplaceMaterial(g, highlight);
						} else {
							// Permanent, pull it under the top container.
							// (reparenting keeps the same worldspace transform)
							g.transform.parent = top.transform;

							SetFlags(g);
							Undo.RegisterCreatedObjectUndo(g, "Stamp Tile");
						}
					}
				}
			}
		}

		return list;
	}

	public GameObject[,,] StampBrush(Vector3Int min, Vector3Int max) {
		return StampBrush(brushSource, min, max, false);
	}

	public void EraseRegion(Vector3Int min, Vector3Int max) {
		GameObject[] erase = FindTiles(min, max);
		foreach (GameObject g in erase)
			Undo.DestroyObjectImmediate(g);
	}

	public void CopyRegion(Vector3Int min, Vector3Int max) {
		Vector3Int size = max - min + Vector3Int.one;
		Brush brush = new Brush(size);
		foreach (GameObject prefab in FindTiles(min, max)) {
			Vector3Int pos = CellAt(prefab.transform.localPosition) - min;
			GameObject g = PrefabUtility.GetCorrespondingObjectFromSource(prefab) as GameObject;
			brush.prefabs[pos.z, pos.y, pos.x] = g;
			brush.dirs[pos.z, pos.y, pos.x] = prefab.transform.localRotation;
		}
		SetBrush(brush);
	}

	public void CutRegion(Vector3Int min, Vector3Int max) {
		CopyRegion(min, max);
		EraseRegion(min, max);
	}

	public void PasteRegion(Vector3Int origin) {
		Vector3Int max = origin + brushSource.size - Vector3Int.one;
		StampBrush(origin, max);
	}

	public GameObject GetContainer() {
		GameObject g = GameObject.Find(tilemapName);
		if (g == null) {
			g = new GameObject(tilemapName);
			Undo.RegisterCreatedObjectUndo(g, "Create Tilemap");
		}
		if (g.GetComponent<TilemapHelper>() == null) {
			TilemapHelper helper = g.AddComponent<TilemapHelper>();
			Undo.RegisterCreatedObjectUndo(helper, "Create Tilemap");
		}
		return g;
	}

	void DrawOutline(Vector3Int min, Vector3Int max, Color color) {
		max += Vector3Int.one;
	
		Color outlineColor = Color.black;
		color.a = 0.1f;
		outlineColor.a = 0.5f;

		GameObject top = GetContainer();

		// Construct the cube corner verts.
		Vector3[] c = new Vector3[8];
		Vector3[] verts = new Vector3[4];
		c[0] = new Vector3(min.x, min.y, min.z);
		c[1] = new Vector3(max.x, min.y, min.z);
		c[2] = new Vector3(min.x, max.y, min.z);
		c[3] = new Vector3(max.x, max.y, min.z);
		c[4] = new Vector3(min.x, min.y, max.z);
		c[5] = new Vector3(max.x, min.y, max.z);
		c[6] = new Vector3(min.x, max.y, max.z);
		c[7] = new Vector3(max.x, max.y, max.z);
		for (int n = 0; n<8; n++)
			c[n] = top.transform.TransformPoint(c[n]);

		// Draw all 6 sides.
		verts[0] = c[0]; verts[1] = c[2]; verts[2] = c[3]; verts[3] = c[1];
		Handles.DrawSolidRectangleWithOutline(verts, color, outlineColor);
		verts[0] = c[4]; verts[1] = c[6]; verts[2] = c[7]; verts[3] = c[5];
		Handles.DrawSolidRectangleWithOutline(verts, color, outlineColor);
		verts[0] = c[1]; verts[1] = c[3]; verts[2] = c[7]; verts[3] = c[5];
		Handles.DrawSolidRectangleWithOutline(verts, color, outlineColor);
		verts[0] = c[4]; verts[1] = c[6]; verts[2] = c[2]; verts[3] = c[0];
		Handles.DrawSolidRectangleWithOutline(verts, color, outlineColor);
		verts[0] = c[2]; verts[1] = c[6]; verts[2] = c[7]; verts[3] = c[3];
		Handles.DrawSolidRectangleWithOutline(verts, color, outlineColor);
		verts[0] = c[1]; verts[1] = c[0]; verts[2] = c[4]; verts[3] = c[5];
		Handles.DrawSolidRectangleWithOutline(verts, color, outlineColor);
	}

	void Select(Vector3Int min, Vector3Int max) {
		hasSelection = true;
		selMin = min;
		selMax = max;

		// Clear out any Unity selection.
		if (mode == Mode.Select && Selection.objects.Length > 0)
			Selection.objects = new Object[0];
	}

	void SetMode(Mode newMode) {
		if (mode == newMode)
			return;
		mode = newMode;
		if (mode != Mode.Select)
			hasSelection = false;
		if (mode != Mode.Draw) {
			brushDisplay.Remove();
		} else {
			if (brushSource != null)
				SetBrush(brushSource);
			else
				SetMode(Mode.Select);
		}
	}

	void DrawCursors(int controlId) {
		if (GUIUtility.hotControl == 0) {
			// Nothing active, so just draw our cursor where it needs to be.
			if (hasCursor) {
				if (Event.current.control || mode == Mode.Pick) {
					brushDisplay.Remove();
					DrawOutline(cursor, cursor, Color.cyan);
				} else if (mode == Mode.Select) {
					DrawOutline(cursor, cursor, Color.cyan);
				} else {
					brushDisplay.MoveTo(cursor);
				}
			} else {
				brushDisplay.Remove();
			}
		} else if (GUIUtility.hotControl == controlId) {
			// Dragging something. Draw a fancy box.
			Vector3Int min, max;
			CalculateBounds(dragStart, dragEnd, out min, out max);
			switch (mode) {
				case Mode.Select: 
				case Mode.Pick:
					DrawOutline(min, max, Color.cyan);
					break;
			}
		}
	}

	void DrawSelectionBox() {
		Vector3Int drawMin = selMin, drawMax = selMax;

		if (!Event.current.alt) {
			Vector3 basePos = (Vector3)(selMin + selMax + Vector3Int.one) * 0.5f;
			GameObject top = GetContainer();

			// Show a 3D handle to allow dragging around.
			int old = GUIUtility.hotControl;
			Vector3 pos = basePos + selDrag;
			pos = top.transform.TransformPoint(pos);
			pos = Handles.PositionHandle(pos, top.transform.rotation);
			pos = top.transform.InverseTransformPoint(pos);
			selDrag = pos - basePos;

			// Track the box to the nearest cell.
			Vector3Int delta = new Vector3Int((int)selDrag.x, (int)selDrag.y, (int)selDrag.z);
			drawMin += delta;
			drawMax += delta;

			// Detect when the handle is let go, and move the tiles.
			if (GUIUtility.hotControl != old) {
				if (GUIUtility.hotControl == 0) {
					CutRegion(selMin, selMax);
					selMin = drawMin;
					selMax = drawMax;
					selDrag = Vector3.zero;
					PasteRegion(selMin);
				}
			}
		}

		DrawOutline(drawMin, drawMax, Color.cyan);
	}

	static Vector3Int MinimumSize(Vector3Int min, Vector3Int max, Brush brush) {
		// If they just pasted a single cell, they probably intended it to
		// paste the whole brush down.
		if (min == max)
			max = min + brush.size - Vector3Int.one;

		// Always paste at least the entire height of the brush.
		max.y = System.Math.Max(max.y, min.y + brush.size.y - 1);
		return max;
	}

	bool DragStart() {
		if (hasCursor) {
			dragStart = dragEnd = cursor;
			if (Event.current.control)
				SetMode(Mode.Pick);
			return true;
		} else {
			hasSelection = false;
			return false;
		}
	}

	void DragUpdate() {
		Vector3Int min, max;
		dragEnd = cursor;
		CalculateBounds(dragStart, dragEnd, out min, out max);
		switch (mode) {
			case Mode.Draw:
				max = MinimumSize(min, max, brushSource);
				brushDisplay.MoveTo(min, max);
				break;
		}
	}

	void DragEnd() {
		Vector3Int min, max;
		CalculateBounds(dragStart, dragEnd, out min, out max);

		switch (mode) {
			case Mode.Select:
				if (Event.current.shift)
					Select(Vector3Int.Min(min, selMin), Vector3Int.Max(max, selMax));
				else
					Select(min, max);
				selDrag = Vector3.zero;
				break;
			case Mode.Draw:
				max = MinimumSize(min, max, brushSource);
				StampBrush(min, max);
				break;
			case Mode.Pick:
				CopyRegion(min, max);
				brushSource.hotspot = dragEnd - min;
				brushSource.hotspot.y = 0;
				SetMode(Mode.Draw);
				break;
		}
	}

	void HandleMouseEvents(int controlId) {
		switch (Event.current.GetTypeForControl(controlId)) {
			case EventType.Layout:
				HandleUtility.AddDefaultControl(controlId);
				break;
			case EventType.MouseDown:
				if (GUIUtility.hotControl == 0 && Event.current.button == 0 && !Event.current.alt) {
					if (DragStart()) {
						GUIUtility.hotControl = controlId;
						Event.current.Use();
					}
				}
				break;
			case EventType.MouseDrag:
				if (GUIUtility.hotControl == controlId && hasCursor) {
					DragUpdate();
					Event.current.Use();
				}
				break;
			case EventType.MouseUp:
				if (GUIUtility.hotControl == controlId && Event.current.button == 0) {
					DragEnd();
					Event.current.Use();
					GUIUtility.hotControl = 0;
				}
				break;
		}
	}

	void HandleKeys() {
		int cmd = -1, mode = -1;
		if (Event.current.type == EventType.KeyDown) {
			switch (Event.current.keyCode ) {
				case KeyCode.S: mode = (int)Mode.Select; break;
				case KeyCode.D: mode = (int)Mode.Draw; break;
				case KeyCode.X: cmd = (int)Command.Cut; break;
				case KeyCode.C: cmd = (int)Command.Copy; break;
				case KeyCode.Q: cmd = (int)Command.Up; break;
				case KeyCode.A: cmd = (int)Command.Down; break;
				case KeyCode.Z: cmd = Event.current.shift ? (int)Command.RotL : (int)Command.RotR; break;
				case KeyCode.Delete: case KeyCode.E: cmd = (int)Command.Erase; break;
				case KeyCode.F: cmd = (int)Command.Fill; break;
			}
		}

		if (cmd != -1) {
			DoCommand((Command)cmd);
			Event.current.Use();
		} else if (mode != -1) {
			SetMode((Mode)mode);
			Event.current.Use();
		}
	}

	void DoCommand(Command cmd) {
		switch (cmd) {
			case Command.Fill:
				if (hasSelection && brushSource != null) {
					StampBrush(selMin, selMax);
					hasSelection = false;
					SetMode(Mode.Select);
				}
				break;
			case Command.Erase:
				if (hasSelection) {
					EraseRegion(selMin, selMax);
					hasSelection = false;
					SetMode(Mode.Select);
				}
				break;
			case Command.Cut:
			case Command.Copy:
				if (hasSelection) {
					if (cmd == Command.Cut)
						CutRegion(selMin, selMax);
					else
						CopyRegion(selMin, selMax);
					brushSource.hotspot.x = brushSource.size.x/2;
					brushSource.hotspot.y = 0;
					brushSource.hotspot.z = brushSource.size.z/2;
					SetMode(Mode.Draw);
				}
				break;
			case Command.Up:
			case Command.Down:
				yPlane += (cmd == Command.Up) ? 1 : -1;
				SetMode(Mode.Draw);
				break;
			case Command.RotL:
				RotateBrush(3);
				break;
			case Command.RotR:
				RotateBrush(1);
				break;
		}
	}

	void Draw2DUI() {
		Handles.BeginGUI();
		GUI.skin = EditorGUIUtility.GetBuiltinSkin(EditorSkin.Scene);
		GUILayout.BeginHorizontal();
		GUILayout.BeginVertical();

		// Show the primary mode toggles.
		for (int i = 0; i<modeNames.Length; i++) {
			float size = 50;
			GUIContent buttonContent = new GUIContent(data.icons[i], modeNames[i]);
			bool isActive = ((int)mode == i);
			if (GUILayout.Toggle(isActive, buttonContent, GUI.skin.button, GUILayout.Width(size), GUILayout.Height(size)))
				SetMode((Mode)i);
		}
		
		// Draw the command button grid.
		GUILayout.Space(10);
		GUILayout.BeginHorizontal();
		int j = 0;
		for (int i = 0; i<cmdNames.Length; i++) {
			float size = 35;
			GUIContent buttonContent = new GUIContent(data.icons[modeNames.Length+i], cmdNames[i]);
			if (GUILayout.Button(buttonContent, GUI.skin.button, GUILayout.Width(size), GUILayout.Height(size)))
				DoCommand((Command)i);

			j++;
			if (j == 2) {
				GUILayout.EndHorizontal();
				GUILayout.BeginHorizontal();
				j = 0;
			}
		}

		// Finish.
		if (j > 0)
			GUILayout.EndHorizontal();
		GUILayout.EndVertical();
		GUILayout.FlexibleSpace();
		GUILayout.EndHorizontal();
		Handles.EndGUI();
	}

	void OnSceneGUI(SceneView sceneView) {
		// If any Unity objects selected, don't show our one.
		if (Selection.objects.Length > 0)
			hasSelection = false;

		GameObject top = GetContainer();
		int controlId = GUIUtility.GetControlID(FocusType.Passive);

		HandleKeys();

		// Find where our 3D cursor is.
		Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
		if (mode == Mode.Draw && !Event.current.control) {
			DrawGrid(sceneView.camera.transform.position);

			// Project the ray onto the 2D plane.
			Vector3 origin = new Vector3(0, (yPlane+0.5f)*gridSize, 0);
			Plane plane = new Plane(top.transform.up, top.transform.TransformPoint(origin));
			float rayT;
			if (plane.Raycast(ray, out rayT)) {
				Vector3 hitPos = ray.GetPoint(rayT);
				hitPos = top.transform.InverseTransformPoint(hitPos);
				cursor = CellAt(hitPos);
				hasCursor = true;
			} else {
				hasCursor = false;
			}
		} else {
			// Find the actual 3D scene intersection.
			if (Event.current.isMouse) {
				object obj = HandleUtility.RaySnap(ray);
				if (obj != null) {
					RaycastHit hit = (RaycastHit)obj;
					Vector3 hitPos = top.transform.InverseTransformPoint(hit.transform.position);
					cursor = CellAt(hitPos);
					hasCursor = true;
				} else {
					hasCursor = false;
				}
			}
		}

		// Draw everything.
		Draw2DUI();
		DrawCursors(controlId);
		if (hasSelection)
			DrawSelectionBox();
		HandleMouseEvents(controlId);

		if (needsRepaint) {
			needsRepaint = false;
			Repaint();
		}

		// 2019 seems to need to force a scene refresh. (?)
		if (Event.current.isMouse) {
			EditorWindow view = EditorWindow.GetWindow<SceneView>();
			view.Repaint();
		}
	}

	// Helper to make a nice grid UI.
	struct TileGrid
	{
		GUIStyle style, hidden, header;
		Texture2D lastTexture;
		int column, tilesPerRow;
		float height;

		public void Start(int _tilesPerRow, float _height) {
			style = new GUIStyle(GUI.skin.button) {
				padding = new RectOffset(4, 4, 4, 4)
			};
			hidden = new GUIStyle(style) {
				contentOffset = new Vector2(0, -9999999)
			};
			header = new GUIStyle(EditorStyles.helpBox) {
				alignment = TextAnchor.UpperCenter,
				fontStyle = FontStyle.Bold,
			};
			tilesPerRow = _tilesPerRow;
			height = Mathf.Floor(_height);
			lastTexture = null;
			column = 0;
			EditorGUILayout.BeginHorizontal();
		}

		public void End() {
			EndRow();
			EditorGUILayout.EndHorizontal();
		}

		public void NewGroup(string name) {
			EndRow();
			EditorGUILayout.EndHorizontal();
			EditorGUILayout.LabelField(name, header);
			EditorGUILayout.BeginHorizontal();
		}

		void EndRow() {
			if (column > 0) {
				// Finish up the row with dummies to keep the sizing consistent.
				using (new EditorGUI.DisabledScope(true)) {
					while (column < tilesPerRow) {
						GUILayout.Toggle(false, new GUIContent(lastTexture), hidden,
							GUILayout.MinWidth(1), GUILayout.Height(height));
						column++;
					}
				}
				column = 0;
			}
		}

		public bool AddTile(bool active, string name, Texture2D texture) {
			lastTexture = texture;
			active = GUILayout.Toggle(active, new GUIContent(texture, name), style,
				GUILayout.MinWidth(1), GUILayout.Height(height));

			column++;
			if (column >= tilesPerRow) {
				EndRow();
				EditorGUILayout.EndHorizontal();
				EditorGUILayout.BeginHorizontal();
			}

			return active;
		}
	}

	void DrawTileSelector() {
		if (tileSet.Length <= 0)
			return;

		TileGrid grid = new TileGrid();
		float height = position.width/tilesPerRow;
		tileScrollPos = EditorGUILayout.BeginScrollView(tileScrollPos);

		grid.Start(tilesPerRow, height);
		string root = tileSetPath+"/";

		// Draw our tiles in rows.
		string prevGroup = "";
		for (int i = 0; i<tileSet.Length; i++) {
			Texture2D preview = AssetPreview.GetAssetPreview(tileSet[i]);
			string name = tileSet[i].name;

			// Extract the group name from the path.
			string group = AssetDatabase.GetAssetPath(tileSet[i]);
			if (group.StartsWith(root))
				group = group.Substring(root.Length);
			int j = group.LastIndexOf('/');
			if (j > 0)
				group = group.Substring(0, j);
			else
				group = "";

			// Remove any numbers the user might have used to order things.
			string[] paths = group.Split('/');
			for (int p = 0; p<paths.Length; p++) {
				Match match = numberSplitter.Match(paths[p]);
				if (match.Success)
					paths[p] = match.Groups[1].Value;
			}
			group = string.Join(" / ", paths);

			// Display a group header when it changes.
			if (group != prevGroup) {
				prevGroup = group;
				grid.NewGroup(group);
			}

			// Update the selection state.
			bool active = (brushSource.size == Vector3Int.one && tileSet[i] == brushSource.prefabs[0,0,0]);
			bool state = grid.AddTile(active, name, preview);

			if (state && !active) {
				SetBrush(tileSet[i]);
				if (!hasSelection)
					SetMode(Mode.Draw);
			}
		}

		grid.End();
		EditorGUILayout.EndScrollView();
	}

	void DrawGridLine(GameObject top, Vector3 a, Vector3 b) {
		Handles.DrawLine(top.transform.TransformPoint(a), top.transform.TransformPoint(b));
	}

	// Draws an infinite grid aligned to Y=up.
	void DrawGrid(Vector3 center) {
		var old = Handles.zTest;
		Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;

		Vector3Int focus = CellAt(center);
		GameObject top = GetContainer();

		int r = 256;
		Vector2Int xRange = new Vector2Int(focus.x-r, focus.x+r);
		Vector2Int zRange = new Vector2Int(focus.z-r, focus.z+r);

		// Get the corners.
		Vector3[] corners = new Vector3[4];
		corners[0] = gridSize * new Vector3(xRange.x, yPlane+0.01f, zRange.x);
		corners[1] = gridSize * new Vector3(xRange.y, yPlane+0.01f, zRange.x);
		corners[2] = gridSize * new Vector3(xRange.y, yPlane+0.01f, zRange.y);
		corners[3] = gridSize * new Vector3(xRange.x, yPlane+0.01f, zRange.y);
		for (int i = 0; i<4; i++)
			corners[i] = top.transform.TransformPoint(corners[i]);

		// Draw a solid plane.
		Color gridColor = new Color(0, 0, 0, 0.2f);
		//Handles.DrawSolidRectangleWithOutline(corners, gridColor, gridColor);

		// Draw gridlines over the top.
		for (int x = xRange.x; x<=xRange.y; x++)
			DrawGridLine(top, new Vector3(x, yPlane, zRange.x)*gridSize, new Vector3(x, yPlane, zRange.y)*gridSize);
		for (int z = zRange.x; z<=zRange.y; z++)
			DrawGridLine(top, new Vector3(xRange.x, yPlane, z)*gridSize, new Vector3(xRange.y, yPlane, z)*gridSize);

		Handles.zTest = old;
	}

	void OnGUI() {
		HandleKeys();

		AssetPreview.SetPreviewTextureCacheSize(tileSet.Length+50);

		// Unity randomly likes to delete this upon scene change and I'm
		// to tired to try and figure out why, so just recreate it when needeed.
		if (headerFont == null)
			headerFont = Font.CreateDynamicFontFromOSFont("Arial", 20);

		// Main header.
		GUIStyle header = new GUIStyle(EditorStyles.helpBox) {
			alignment = TextAnchor.UpperCenter,
			fontStyle = FontStyle.Bold,
			font = headerFont,
		};
		EditorGUILayout.LabelField("Tilemap Editor", header, GUILayout.Height(28));

		// Options.
		EditorGUILayout.BeginHorizontal("box");
		EditorGUILayout.BeginVertical();
		gridSize = EditorGUILayout.FloatField("Grid Size", gridSize);
		tileSetPath = EditorGUILayout.TextField("Tileset Path", tileSetPath);
		tilesPerRow = Mathf.Max(1, EditorGUILayout.IntField("Tiles Per Row", tilesPerRow));
		EditorGUILayout.EndVertical();
		if (GUILayout.Button("Reload Tiles", GUILayout.ExpandHeight(true)))
			ReloadTiles();
		EditorGUILayout.EndHorizontal();

		// Main tiles.
		EditorGUILayout.BeginVertical();
		DrawTileSelector();
		EditorGUILayout.EndVertical();

		GUILayout.FlexibleSpace();

		// If we're still loading previews, trigger another repaint until we're done.
		if (AssetPreview.IsLoadingAssetPreviews())
			needsRepaint = true;
	}
}
