using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;

/// <summary>Tool used to find unused assets</summary>
public class UnusedAssetsFinder : EditorWindow
{
	private const string RESULTS_KEY = nameof(UnusedAssetsFinder) + "_Results";
	private const int MAX_FILE_SIZE = 100000;
	private const string BUILD_SETTINGS_FLAG = "BuildSettings";
	private const string ASSET_BUNDLE_FLAG = "(Asset bundle) ";
	private const string GUID_FLAG = "guid:";
	private const int RESULTS_PER_PAGE = 30;
	private const int FILTER_WIDTH = 200;
	private const int PREVIEW_WIDTH = 60;
	private const float GREY_VALUE = 0.8f;
	private const int TARGET_FPS = 30;
	private const string RECOVERY_DIR = ".recovery";

	private static readonly string[] UNITY_EXTENSIONS = new string[] {
		// 3D model
		".fbx", // 0
		".mb",
		".ma",
		".max",
		".jas",
		".dae",
		".dxf",
		".obj",
		".c4d",
		".blend",
		".lxo",
		".mesh",
		".3ds",
		".skp", // 13
		// Animation
		".anim", // 14
		".controller",
		".overrideController",
		".blendtree",
		".animset",
		".playable",
		".state",
		".statemachine",
		".signal",
		".transition", // 23
		// Assembly
		".asmdef", // 24
		".asmref", // 25
		// Audio
		".ogg", // 26
		".aif",
		".aiff",
		".flac",
		".wav",
		".mp3",
		".mod",
		".it",
		".s3m",
		".xm",
		".mixer", // 36
		// Avatar
		".ht", // 37
		".mask", // 38
		// Build report
		".buildreport", // 39
		// Curves
		".curves", // 40
		".curvesNormalized", // 41
		// Font
		".fontsettings", // 42
		".ttf",
		".dfont",
		".otf",
		".ttc", // 46
		// Independent hardware vendor
		".astc", // 47
		".dds",
		".ktx",
		".pvr", // 50
		// Localization
		".po", // 51
		// Material
		".mat" , // 52
		".cubemap" ,
		".physicMaterial" ,
		".physicsMaterial2D" , // 55
		// Plugin
		".dll", // 56
		".winmd",
		".so",
		".jar",
		".java",
		".kt",
		".aar",
		".suprx",
		".prx",
		".rpl",
		".cpp",
		".cc",
		".c",
		".h",
		".jslib",
		".jspre",
		".bc",
		".a",
		".m",
		".mm",
		".swift",
		".xib",
		".bundle",
		".dylib",
		".config", // 80
		// Prefab
		".prefab", // 81
		// Preset
		".preset", // 82
		// Scene
		".rsp", // 83
		".unity", // 84
		// Script
		".cs", // 85
		// Scriptable object
		".asset", // 86
		// Shader
		".compute", // 87
		".raytrace",
		".cginc",
		".cg",
		".glslinc",
		".hlsl",
		".shader",
		".shadervariants",
		".shadergraph",
		".shadersubgraph", // 96
		// Substance
		".sbsar", // 97
		// Colors
		".colors", // 98
		".gradients", // 99
		// Terrain
		".brush", // 100
		".terrainlayer",
		".spm",
		".st", // 103
		// Texture
		".jpg", // 104
		".jpeg",
		".tif",
		".tiff",
		".tga",
		".gif",
		".png",
		".psd",
		".bmp",
		".iff",
		".pict",
		".pic",
		".pct",
		".exr",
		".hdr",
		".renderTexture",
		".texture2D",
		".spriteatlas",
		".webCamTexture", // 122
		// GUI skin
		".guiskin", // 123
		// Video
		".avi", // 124
		".asf",
		".wmv",
		".mov",
		".dv",
		".mp4",
		".m4v",
		".mpg",
		".mpeg",
		".ogv",
		".vp8",
		".webm", // 135
		// Visual effects
		".flare", // 136
		".giparams",
		".vfx",
		".vfxoperator",
		".vfxblock",
		".particleCurves",
		".particleCurvesSigned",
		".particleDoubleCurves",
		".particleDoubleCurvesSigned",
		".lighting" // 145
	};

	private static readonly int[] YAML_INDEXES = new int[] { 2, 5, 7, 9, 12, 14, 15, 18, 21, 22, 24 };

	private static UnusedAssetsFinder _instance;
	private static UnusedAssetsFinder Instance
	{
		get
		{
			if (_instance == null)
				RefreshWindow();

			return _instance;
		}
	}

	private static string recoveryDir => Path.Combine(Application.dataPath, RECOVERY_DIR);

	private enum AssetType
	{
		None,
		_3D_model,
		Animation,
		Assembly,
		Audio,
		Avatar,
		Build_report,
		Curves,
		Editor_tool,
		Font,
		Independent_hardware_vendor,
		Localization,
		Material,
		Plugin,
		Prefab,
		Preset,
		Scene,
		Script,
		Scriptable_object,
		Shader,
		Substance,
		Colors,
		Terrain,
		Texture,
		GUI_skin,
		Video,
		Visual_effect,
		Text
	}

	private enum AnalysisStatus
	{
		Before,
		Running_Sync,
		Async_Asset_Indexing,
		Async_Analysis,
		Async_Ref_Analysis,
		Results
	}

	private float pathWidth => Instance.minSize.x - 20;
	private float labelWidth => pathWidth - FILTER_WIDTH - PREVIEW_WIDTH - 30;
	private Color fadedColor => Color.white * GREY_VALUE;
	private int maxPageIndex => Mathf.FloorToInt((float)selectedSortResults.Count / RESULTS_PER_PAGE);

	// styles
	private GUIStyle titleStyle;
	private GUIStyle rightStyle;
	private GUIStyle textStyle;
	private GUIStyle buttonStyle;
	private GUIStyle centerStyle;

	// shared
	private AnalysisResults results;
	private Dictionary<string, string> resPathToGuid;
	private Dictionary<string, List<string>> classToGuids;
	private Dictionary<string, string> classToEditor;
	private Dictionary<string, List<string>> guidToReferences;
	private List<KeyValuePair<string, string>> selectedRefResults;
	private List<KeyValuePair<string, string>> selectedSearchResults;
	private List<KeyValuePair<string, string>> selectedSortResults;
	private List<KeyValuePair<string, string>> selectedPageResults;
	private Dictionary<string, bool> guidToFoldoutStatus;
	private Dictionary<string, bool> guidToSelectedStatus;
	private Vector2 scroll;
	private AnalysisStatus status;
	private int progressIndex;
	private bool forceSearch;
	private bool forceSort;


	// async part
	private bool cancelFlag;
	private string[] asyncAssetPaths;
	private List<KeyValuePair<string, List<string>>> asyncAssetBundles;
	private int asyncMaxIndex;
	private List<string> asyncAssetToValidate;

	private bool _showReferencedAssets;
	private bool ShowReferencedAssets
	{
		get => _showReferencedAssets;
		set
		{
			_showReferencedAssets = value;
			selectedRefResults = new List<KeyValuePair<string, string>>();

			foreach (KeyValuePair<string, string> pair in results.guidToPath)
			{
				if (results.guidToRefStatus[pair.Key] == value)
					selectedRefResults.Add(pair);
			}

			forceSearch = true;
			Search = Search;
		}
	}

	private string _search;
	private string Search
	{
		get => _search;
		set
		{
			if (!forceSearch && _search == value)
				return;

			selectedSearchResults = new List<KeyValuePair<string, string>>();

			foreach (KeyValuePair<string, string> pair in selectedRefResults)
			{
				if (string.IsNullOrEmpty(value))
				{
					selectedSearchResults.Add(pair);
					continue;
				}

				FileInfo info = new FileInfo(pair.Value);

				if (info.Name.ToLower().Trim().Contains(value.ToLower().Trim()))
					selectedSearchResults.Add(pair);
			}

			_search = value;
			forceSearch = false;

			forceSort = true;
			SortType = SortType;
		}
	}

	private int _sortType;
	private int SortType
	{
		get => _sortType;
		set
		{
			if (!forceSort && _sortType == value)
				return;

			selectedSortResults = new List<KeyValuePair<string, string>>();

			foreach (KeyValuePair<string, string> pair in selectedSearchResults)
			{
				FileInfo info = new FileInfo(pair.Value);

				if (info.Exists && (value == (int)AssetType.None || (int)GetAssetType(info) == value))
					selectedSortResults.Add(pair);
			}

			_sortType = value;
			forceSort = false;
			PageIndex = 0;
		}
	}

	private int _pageIndex;
	private int PageIndex
	{
		get => _pageIndex;
		set
		{
			_pageIndex = value;

			selectedPageResults = new List<KeyValuePair<string, string>>();
			guidToFoldoutStatus = new Dictionary<string, bool>();
			int maxIndex = Mathf.Min((value + 1) * RESULTS_PER_PAGE, selectedSortResults.Count);

			for (int i = value * RESULTS_PER_PAGE; i < maxIndex; i++)
			{
				selectedPageResults.Add(selectedSortResults[i]);
				guidToFoldoutStatus.Add(selectedPageResults[^1].Key, false);
			}

			scroll = Vector2.zero;
		}
	}

	private string[] _assetTypes;
	private string[] AssetTypes
	{
		get
		{
			if (_assetTypes == null)
			{
				List<string> names = new List<string>();

				foreach (AssetType type in Enum.GetValues(typeof(AssetType)))
					names.Add(CleanAssetType(type));

				_assetTypes = names.ToArray();
			}

			return _assetTypes;
		}
	}

	// Main tool flow
	[MenuItem("Tools/Unused Assets Finder")]
	public static void ShowEditorWindow()
	{
		RefreshWindow();

		// create recovery folder
		if (!Directory.Exists(recoveryDir))
			Directory.CreateDirectory(recoveryDir);

		// add to .gitignore
		string gitIgnorePath = Application.dataPath.Replace("Assets", ".gitignore");

		if (File.Exists(gitIgnorePath))
		{
			List<string> lines = new List<string>(File.ReadAllLines(gitIgnorePath));
			lines.Add("# Unused Assets Finder recovery folder");
			lines.Add("/[Aa]ssets/.recovery");
			lines.Add("");

			File.WriteAllLines(gitIgnorePath, lines);

			EditorUtility.DisplayDialog(
				".gitignore updated",
				"Your .gitignore file has been updated to not detect the recovery folder",
				"Okay"
			);
		}

		AssemblyReloadEvents.beforeAssemblyReload += CancelOperations;

		if (PlayerPrefs.HasKey(RESULTS_KEY))
			AskForLoading();
		else
			AskForProcess();
	}

	private static void AskForLoading()
	{
		string message = "Do you want to load the previous project analysis or start a new one ?";

		if (EditorUtility.DisplayDialog("Loading", message, "Load last results", "Analyze project"))
			LoadResults();
		else
			AskForProcess();
	}

	private static void LoadResults()
	{
		Instance.cancelFlag = false;

		if (!PlayerPrefs.HasKey(RESULTS_KEY))
			return;

		Instance.results = JsonUtility.FromJson<AnalysisResults>(PlayerPrefs.GetString(RESULTS_KEY));
		Instance.results.PostSerialization();

		Instance.status = AnalysisStatus.Results;
		Instance.ShowReferencedAssets = true;
		GenerateSelectionDictionary();
	}

	private static void AskForProcess()
	{
		string message = "This tool can run in synchronous mode (editor will be blocked during the process, but it will take less time) or asynchronous mode (editor won't be locked but it will take longer).";

		if (EditorUtility.DisplayDialog("Asset analysis", message, "Start sync", "Start async"))
		{
			SyncAssetsIndexing();
			SyncAnalysis();
			SyncRefAnalysis();
		}
		else
		{
			Instance.Show();
			StartAsyncIndexing();
		}
	}

	private static void StartAsyncIndexing()
	{
		Instance.cancelFlag = false;

		Instance.results.guidToPath = new Dictionary<string, string>();
		Instance.resPathToGuid = new Dictionary<string, string>();
		Instance.classToGuids = new Dictionary<string, List<string>>();
		Instance.classToEditor = new Dictionary<string, string>();
		Instance.progressIndex = 0;
		Instance.status = AnalysisStatus.Async_Asset_Indexing;

		EditorApplication.update += AsyncAssetIndexing;
	}

	private static void SyncAssetsIndexing()
	{
		Instance.status = AnalysisStatus.Running_Sync;

		Instance.results.guidToPath = new Dictionary<string, string>();
		Instance.resPathToGuid = new Dictionary<string, string>();
		Instance.classToGuids = new Dictionary<string, List<string>>();
		Instance.classToEditor = new Dictionary<string, string>();

		string[] paths = AssetDatabase.GetAllAssetPaths();
		FileInfo file;
		Instance.progressIndex = 0;

		foreach (string path in paths)
		{
			file = new FileInfo(path);

			if (IsProjectAsset(file))
			{
				string guid = GetAssetGUID(path);
				Instance.results.guidToPath.Add(guid, path);

				if (path.Contains("Resources"))
					Instance.resPathToGuid.Add(path, guid);

				if (GetAssetType(file) == AssetType.Script || GetAssetType(file) == AssetType.Editor_tool)
					ExtractClassNames(guid, path);
			}

			EditorUtility.DisplayProgressBar(
				"Step 1 : Indexing assets",
				"Indexing project assets and GUIDs (" + Instance.progressIndex + "/" + paths.Length + ")",
				(float)Instance.progressIndex / paths.Length
			);

			Instance.progressIndex++;
		}

		EditorUtility.ClearProgressBar();
	}

	private static void AsyncAssetIndexing()
	{
		if (CheckInterruption())
			return;

		long ticks = DateTime.Now.Ticks;
		long maxTicks = ticks + (10000000 / TARGET_FPS);

		if (Instance.asyncAssetPaths == null)
			Instance.asyncAssetPaths = AssetDatabase.GetAllAssetPaths();

		FileInfo file;
		string path;

		while (Instance.progressIndex < Instance.asyncAssetPaths.Length)
		{
			if (CheckInterruption())
				return;

			path = Instance.asyncAssetPaths[Instance.progressIndex];
			file = new FileInfo(path);

			if (IsProjectAsset(file))
			{
				string guid = GetAssetGUID(path);
				Instance.results.guidToPath.Add(guid, path);

				if (path.Contains("Resources"))
					Instance.resPathToGuid.Add(path, guid);

				if (GetAssetType(file) == AssetType.Script || GetAssetType(file) == AssetType.Editor_tool)
					ExtractClassNames(guid, path);
			}

			Instance.progressIndex++;

			// interruption
			if (DateTime.Now.Ticks >= maxTicks)
				return;
		}

		EditorApplication.update -= AsyncAssetIndexing;

		Instance.status = AnalysisStatus.Async_Analysis;
		Instance.guidToReferences = new Dictionary<string, List<string>>();
		Instance.results.guidToSources = new Dictionary<string, List<string>>();
		Instance.progressIndex = 0;

		EditorApplication.update += AsyncAnalysis;
	}

	private static void SyncAnalysis()
	{
		Instance.guidToReferences = new Dictionary<string, List<string>>();
		Instance.results.guidToSources = new Dictionary<string, List<string>>();
		int maxIndex = Instance.results.guidToPath.Count;
		Instance.progressIndex = 0;

		foreach (KeyValuePair<string, string> asset in Instance.results.guidToPath)
		{
			ManageFile(asset.Value, asset.Key);

			EditorUtility.DisplayProgressBar(
				"Step 2 : Analyzing references",
				"Analyzing project asset references (" + Instance.progressIndex + "/" + maxIndex + ")",
				(float)Instance.progressIndex / maxIndex
			);

			Instance.progressIndex++;
		}

		EditorUtility.ClearProgressBar();
	}

	private static void AsyncAnalysis()
	{
		if (CheckInterruption())
			return;

		long ticks = DateTime.Now.Ticks;
		long maxTicks = ticks + (10000000 / TARGET_FPS);

		while (Instance.progressIndex < Instance.results.guidToPath.Count)
		{
			if (CheckInterruption())
				return;

			KeyValuePair<string, string> asset = Instance.results.guidToPath.ElementAt(Instance.progressIndex);
			ManageFile(asset.Value, asset.Key);

			Instance.progressIndex++;

			// interruption
			if (DateTime.Now.Ticks >= maxTicks)
				return;
		}

		EditorApplication.update -= AsyncAnalysis;

		Instance.status = AnalysisStatus.Async_Ref_Analysis;
		Instance.results.guidToRefStatus = new Dictionary<string, bool>();
		Instance.progressIndex = 0;

		foreach (KeyValuePair<string, string> pair in Instance.results.guidToPath)
			Instance.results.guidToRefStatus.Add(pair.Key, false);

		EditorApplication.update += AsyncRefAnalysis;
	}

	private static void SyncRefAnalysis()
	{
		Instance.results.guidToRefStatus = new Dictionary<string, bool>();

		foreach (KeyValuePair<string, string> pair in Instance.results.guidToPath)
			Instance.results.guidToRefStatus.Add(pair.Key, false);

		List<KeyValuePair<string, List<string>>> assetBundles = new List<KeyValuePair<string, List<string>>>(
			Instance.guidToReferences.Where(item => item.Key.Contains(ASSET_BUNDLE_FLAG))
		);
		int maxIndex = SceneManager.sceneCount + assetBundles.Count;
		Instance.progressIndex = 0;
		List<string> assetsToValidate = new List<string>();
		int index = 0;

		// start with scenes
		while (index < SceneManager.sceneCount)
		{
			string sceneGuid = GetAssetGUID(SceneManager.GetSceneAt(index).path);
			assetsToValidate.Add(sceneGuid);

			EditorUtility.DisplayProgressBar(
				"Step 3 : Analyzing reference chains",
				"Analyzing chains of references (" + Instance.progressIndex + "/" + maxIndex + ")",
				(float)Instance.progressIndex / maxIndex
			);

			Instance.progressIndex++;
			index++;
		}

		// add asset bundles
		index = 0;
		while (index < assetBundles.Count)
		{
			assetBundles[index].Value.ForEach(guid => assetsToValidate.Add(guid));

			EditorUtility.DisplayProgressBar(
				"Step 3 : Analyzing reference chains",
				"Analyzing chains of references (" + Instance.progressIndex + "/" + maxIndex + ")",
				(float)Instance.progressIndex / maxIndex
			);

			Instance.progressIndex++;
			index++;
		}

		// start process
		index = 0;
		while (index < assetsToValidate.Count)
		{
			RegisterAssetRef(assetsToValidate, assetsToValidate[index]);

			EditorUtility.DisplayProgressBar(
				"Step 3 : Analyzing reference chains",
				"Analyzing chains of references (" + Instance.progressIndex + "/" + (maxIndex + assetsToValidate.Count) + ")",
				(float)Instance.progressIndex / (maxIndex + assetsToValidate.Count)
			);

			Instance.progressIndex++;
			index++;
		}

		EditorUtility.ClearProgressBar();
		Instance.status = AnalysisStatus.Results;
		Instance.ShowReferencedAssets = true;
		GenerateSelectionDictionary();

		Instance.results.PreSerialization();
		PlayerPrefs.SetString(RESULTS_KEY, JsonUtility.ToJson(Instance.results));
	}

	private static void AsyncRefAnalysis()
	{
		if (CheckInterruption())
			return;

		long ticks = DateTime.Now.Ticks;
		long maxTicks = ticks + (10000000 / TARGET_FPS);

		if (Instance.progressIndex == 0)
		{
			Instance.asyncAssetBundles = new List<KeyValuePair<string, List<string>>>(
				Instance.guidToReferences.Where(item => item.Key.Contains(ASSET_BUNDLE_FLAG))
			);
			Instance.asyncMaxIndex = SceneManager.sceneCount + Instance.asyncAssetBundles.Count;

			Instance.asyncAssetToValidate = new List<string>();
			string sceneGuid;
			int index = 0;

			// start with scene
			while (index < SceneManager.sceneCount)
			{
				sceneGuid = GetAssetGUID(SceneManager.GetSceneAt(Instance.progressIndex).path);
				Instance.asyncAssetToValidate.Add(sceneGuid);

				Instance.progressIndex++;
				index++;
			}

			// add asset bundle
			index = 0;
			while (index < Instance.asyncAssetBundles.Count)
			{
				Instance.asyncAssetBundles[index].Value.ForEach(guid => Instance.asyncAssetToValidate.Add(guid));

				Instance.progressIndex++;
				index++;
			}
		}

		if (CheckInterruption())
			return;

		string guid;

		while (Instance.progressIndex - Instance.asyncMaxIndex < Instance.asyncAssetToValidate.Count)
		{
			if (CheckInterruption())
				return;

			guid = Instance.asyncAssetToValidate[Instance.progressIndex - Instance.asyncMaxIndex];
			RegisterAssetRef(Instance.asyncAssetToValidate, guid);

			Instance.progressIndex++;

			// interruption
			if (DateTime.Now.Ticks >= maxTicks)
				return;
		}

		EditorApplication.update -= AsyncRefAnalysis;

		Instance.status = AnalysisStatus.Results;
		Instance.ShowReferencedAssets = true;
		GenerateSelectionDictionary();

		Instance.results.PreSerialization();
		PlayerPrefs.SetString(RESULTS_KEY, JsonUtility.ToJson(Instance.results));

		EditorUtility.DisplayDialog(
			"Async analysis finished.",
			"The async analysis of this project is finished.",
			"Close"
		);
	}

	private void OnGUI()
	{
		GenerateIfNeeded();

		EditorGUILayout.LabelField("Unused assets finder", titleStyle);
		EditorGUILayout.Space();

		switch (status)
		{
			case AnalysisStatus.Before:
				CenterDisplay(() =>
				{
					EditorGUILayout.HelpBox(
						"The operations have been interrupted, likely because of an assembly reload.",
						MessageType.Warning
					);
				});

				GUIDivider();

				CenterDisplay(() =>
				{
					if (PlayerPrefs.HasKey(RESULTS_KEY) && GUILayout.Button("Load last analysis"))
						LoadResults();

					EditorGUILayout.Space();

					if (GUILayout.Button("Sync analysis"))
					{
						SyncAssetsIndexing();
						SyncAnalysis();
						SyncRefAnalysis();
					}

					EditorGUILayout.Space();

					if (GUILayout.Button("Start async"))
						StartAsyncIndexing();
				});
				break;

			case AnalysisStatus.Async_Asset_Indexing:
				AsyncLoadingBar(
					"Step 1 : Indexing assets",
					(float)progressIndex / asyncAssetPaths.Length
				);
				break;

			case AnalysisStatus.Async_Analysis:
				AsyncLoadingBar(
					"Step 2 : Analyzing references",
					(float)progressIndex / results.guidToPath.Count
				);
				break;

			case AnalysisStatus.Async_Ref_Analysis:
				AsyncLoadingBar(
					"Step 3 : Analyzing reference chains",
					(float)progressIndex / asyncMaxIndex
				);
				break;

			case AnalysisStatus.Results:
				CenterDisplay(() =>
				{
					if (PlayerPrefs.HasKey(RESULTS_KEY) && GUILayout.Button("Load last analysis"))
						LoadResults();

					EditorGUILayout.Space();

					if (GUILayout.Button("Reload sync"))
					{
						SyncAssetsIndexing();
						SyncAnalysis();
						SyncRefAnalysis();
					}

					EditorGUILayout.Space();

					if (GUILayout.Button("Reload async"))
						StartAsyncIndexing();
				});

				GUIDivider();

				DisplayResults();
				break;
		}

		if (cancelFlag)
			AssemblyReloadEvents.beforeAssemblyReload -= CancelOperations;
	}

	private void DisplayResults()
	{
		// Mode selection
		CenterDisplay(() =>
		{
			Color color = ShowReferencedAssets ? Color.cyan : Color.white;

			DisplayColored(() =>
			{
				if (GUILayout.Button("Referenced assets") && !ShowReferencedAssets)
					ShowReferencedAssets = true;
			}, color);

			EditorGUILayout.Space();
			color = ShowReferencedAssets ? Color.white : Color.cyan;

			DisplayColored(() =>
			{
				if (GUILayout.Button("Unused assets") && ShowReferencedAssets)
					ShowReferencedAssets = false;
			}, color);

			EditorGUILayout.Space();
			SortType = EditorGUILayout.Popup(SortType, AssetTypes, GUILayout.Width(FILTER_WIDTH));
		});

		GUIDivider();

		// Assets operations
		CenterDisplay(() =>
		{
			EditorGUILayout.LabelField("Search : ", rightStyle, GUILayout.Width(65));
			Search = EditorGUILayout.TextField(Search);

			bool hasSelection = guidToSelectedStatus.Count(item => item.Value) > 0;
			bool hasRestore = Directory.GetFiles(recoveryDir).Length + Directory.GetDirectories(recoveryDir).Length > 0;

			if (hasSelection)
			{
				EditorGUILayout.Space();

				DisplayColored(
					() =>
					{
						if (GUILayout.Button("Remove assets"))
						{
							string message = "Are you sure you want to remove the selected " + guidToSelectedStatus.Count(item => item.Value) + " assets ?\n\nThe selected assests will be moved to a recovery folder instead of being deleted. They can be recovered with the \"Restore assets\" button.\n\nAssets in the recovery folder with the same name will be deleted.";

							if (EditorUtility.DisplayDialog("Asset removal", message, "Yes", "No"))
								MoveAssetsToRecovery();
						}
					},
					Color.red
				);
			}

			if (hasRestore)
			{
				EditorGUILayout.Space();

				DisplayColored(
					() =>
					{
						if (hasRestore && GUILayout.Button("Restore assets"))
						{
							string message = "Are you sure you want to recover all removed assets ?\nThis action will delete assets with the same name as recovered assets in the asset folder.";

							if (EditorUtility.DisplayDialog("Asset recovery", message, "Yes", "No"))
								RecoverAssets();
						}
					},
					Color.yellow
				);
			}
		});

		EditorGUILayout.Space();

		// Asset selection and titles
		EditorGUILayout.BeginHorizontal(new GUIStyle(GUI.skin.box));
		{
			int selectionState = 0;
			bool hasUnselected = false;

			foreach (KeyValuePair<string, string> pair in selectedSortResults)
			{
				// need this to fix race condition
				if (!guidToSelectedStatus.ContainsKey(pair.Key))
					continue;

				if (guidToSelectedStatus[pair.Key])
					selectionState = 2;
				else
					hasUnselected = true;
			}

			if (selectionState == 2 && hasUnselected)
				selectionState = 1;

			EditorGUI.showMixedValue = selectionState == 1;
			bool newState = EditorGUILayout.Toggle(selectionState == 2);
			EditorGUI.showMixedValue = false;

			if (selectedSortResults.Count != 0)
			{
				if (newState)
				{
					if (selectionState == 0 || selectionState == 1)
					{
						foreach (KeyValuePair<string, string> pair in selectedSortResults)
							guidToSelectedStatus[pair.Key] = true;
					}
				}
				else if (selectionState == 2)
				{
					foreach (KeyValuePair<string, string> pair in selectedSortResults)
						guidToSelectedStatus[pair.Key] = false;
				}
			}

			EditorGUILayout.LabelField("Asset name", textStyle, GUILayout.Width(labelWidth));
			EditorGUILayout.LabelField("(Asset type)  ", rightStyle, GUILayout.Width(FILTER_WIDTH));
		}
		EditorGUILayout.EndHorizontal();

		EditorGUILayout.Space();

		// Display assets
		FileInfo info;

		scroll.x = 0;
		scroll = EditorGUILayout.BeginScrollView(scroll);
		{
			foreach (KeyValuePair<string, string> asset in selectedPageResults)
			{
				if (!asset.Equals(selectedPageResults.ElementAt(0)))
					DisplayColored(() => GUIDivider(), fadedColor);

				info = new FileInfo(asset.Value);

				// we need this to fix race issue
				if (!guidToSelectedStatus.ContainsKey(asset.Key))
					continue;

				EditorGUILayout.BeginHorizontal();
				{
					EditorGUILayout.Space();

					guidToSelectedStatus[asset.Key] = EditorGUILayout.Toggle(guidToSelectedStatus[asset.Key]);

					if (GUILayout.Button("Preview", GUILayout.Width(60)))
						PreviewPopup.Preview(asset.Value, GetAssetType(info));

					EditorGUILayout.LabelField(
						info.Name.Replace(info.Extension, ""),
						textStyle,
						GUILayout.Width(labelWidth)
					);

					DisplayColored(
						() => EditorGUILayout.LabelField(
							"(" + CleanAssetType(GetAssetType(info)) + ")",
							rightStyle,
							GUILayout.Width(FILTER_WIDTH)
						),
						fadedColor
					);
				}
				EditorGUILayout.EndHorizontal();

				DisplayColored(() =>
					EditorGUILayout.LabelField(asset.Value, textStyle, GUILayout.Width(pathWidth)),
					fadedColor
				);

				if (!results.guidToSources.ContainsKey(asset.Key))
					continue;

				DisplayColored(
					() =>
					{
						guidToFoldoutStatus[asset.Key] = EditorGUILayout.BeginFoldoutHeaderGroup(
							guidToFoldoutStatus[asset.Key],
							"Referenced in"
						);
						{
							if (guidToFoldoutStatus[asset.Key])
							{
								DisplayColored(() =>
								{
									foreach (string guid in results.guidToSources[asset.Key])
									{
										if (results.guidToPath.ContainsKey(guid))
											EditorGUILayout.LabelField("-\u00A0" + results.guidToPath[guid], textStyle);
										else
											EditorGUILayout.LabelField("-\u00A0" + guid, textStyle);
									}
								}, Color.white);
							}
						}
						EditorGUILayout.EndFoldoutHeaderGroup();
					},
					fadedColor
				);
			}
		}
		EditorGUILayout.EndScrollView();

		GUIDivider();
		EditorGUILayout.Space();

		// Page selection
		CenterDisplay(() =>
		{
			if (PageIndex > 0 && GUILayout.Button("<", GUILayout.Width(50)))
				PageIndex--;

			EditorGUILayout.LabelField(
				"page " + (PageIndex + 1) + "/" + (maxPageIndex + 1),
				centerStyle,
				GUILayout.Width(100)
			);

			if (PageIndex < maxPageIndex && GUILayout.Button(">", GUILayout.Width(50)))
				PageIndex++;
		});

		EditorGUILayout.Space();
		EditorGUILayout.Space();
	}

	private void MoveAssetsToRecovery()
	{
		// select assets
		List<(string, string)> selectedAssets = new List<(string, string)>();

		foreach (KeyValuePair<string, bool> pair in guidToSelectedStatus)
		{
			if (pair.Value)
				selectedAssets.Add((pair.Key, results.guidToPath[pair.Key]));
		}

		// delete assets
		progressIndex = 0;
		foreach ((string guid, string path) asset in selectedAssets)
		{
			DereferenceAsset(asset.guid);
			MoveAsset(
				Path.GetFullPath(asset.path),
				Path.Combine(recoveryDir, asset.path.Replace("\\", "/").Replace("Assets/", ""))
			);
			MoveAsset(
				Path.GetFullPath(asset.path) + ".meta",
				Path.Combine(recoveryDir, asset.path.Replace("\\", "/").Replace("Assets/", "")) + ".meta"
			);

			EditorUtility.DisplayProgressBar(
				"Deleting assets",
				"Deleting assets (" + progressIndex + "/" + selectedAssets.Count + ").",
				(float)progressIndex / selectedAssets.Count
			);

			progressIndex++;
		}

		EditorUtility.ClearProgressBar();

		string message = "All selected assets have been removed.";
		EditorUtility.DisplayDialog("Asset deletion done", message, "Ok");

		AssetDatabase.Refresh();
	}

	private void RecoverAssets()
	{
		List<string> recoveryAssets = GetFilesRecursive(recoveryDir);

		progressIndex = 0;
		foreach (string path in recoveryAssets)
		{
			string newPath = path.Replace("\\", "/").Replace(RECOVERY_DIR + "/", "");
			MoveAsset(path, newPath);

			EditorUtility.DisplayProgressBar(
				"Recovering assets",
				"Recovering assets (" + progressIndex + "/" + recoveryAssets.Count + ").",
				(float)progressIndex / recoveryAssets.Count
			);

			progressIndex++;
		}

		// clear .recovery folders
		foreach (string recoveryDirPath in Directory.GetDirectories(recoveryDir))
			Directory.Delete(recoveryDirPath, true);

		EditorUtility.ClearProgressBar();

		string message = "Assets have been recovered, new analysis required.";
		EditorUtility.DisplayDialog("Asset recovery done", message, "Ok");

		AssetDatabase.Refresh();
		status = AnalysisStatus.Before;
	}

	// Utility methods
	private static void RefreshWindow()
	{
		_instance = GetWindow<UnusedAssetsFinder>();
		_instance.titleContent = new GUIContent("Unused Assets Finder");
		_instance.minSize = new Vector2(600, 700);
		_instance.results = new AnalysisResults();
	}

	private static bool IsProjectAsset(FileInfo info)
	{
		return info.FullName.Replace("\\", "/").Contains(Application.dataPath) && info.Extension != string.Empty;
	}

	private static string GetAssetGUID(string path)
	{
		return AssetDatabase.AssetPathToGUID(path, AssetPathToGUIDOptions.OnlyExistingAssets);
	}

	private static AssetType GetAssetType(FileInfo info)
	{
		int index = new List<string>(UNITY_EXTENSIONS).IndexOf(info.Extension);

		// fix for upper extensions
		if (index == -1)
		{
			index = 0;

			foreach (string extension in UNITY_EXTENSIONS)
			{
				if (info.Extension.ToLower() == extension.ToLower())
					break;

				index++;
			}
		}

		if (index >= 0 && index <= 13)
			return AssetType._3D_model;

		if (index >= 14 && index <= 23)
			return AssetType.Animation;

		if (index >= 24 && index <= 25)
			return AssetType.Assembly;

		if (index >= 26 && index <= 36)
			return AssetType.Audio;

		if (index >= 37 && index <= 38)
			return AssetType.Avatar;

		if (index == 39)
			return AssetType.Build_report;

		if (index >= 40 && index <= 41)
			return AssetType.Curves;

		if (index >= 42 && index <= 46)
			return AssetType.Font;

		if (index >= 47 && index <= 50)
			return AssetType.Independent_hardware_vendor;

		if (index == 51)
			return AssetType.Localization;

		if (index >= 52 && index <= 55)
			return AssetType.Material;

		if (index >= 56 && index <= 80)
			return AssetType.Plugin;

		if (index == 81)
			return AssetType.Prefab;

		if (index == 82)
			return AssetType.Preset;

		if (index >= 83 && index <= 84)
			return AssetType.Scene;

		if (index == 85)
		{
			if (File.ReadAllText(info.FullName).Contains("using UnityEditor;"))
				return AssetType.Editor_tool;

			return AssetType.Script;
		}

		if (index == 86)
			return AssetType.Scriptable_object;

		if (index >= 87 && index <= 96)
			return AssetType.Shader;

		if (index == 97)
			return AssetType.Substance;

		if (index >= 98 && index <= 99)
			return AssetType.Colors;

		if (index >= 100 && index <= 103)
			return AssetType.Terrain;

		if (index >= 104 && index <= 122)
			return AssetType.Texture;

		if (index == 123)
			return AssetType.GUI_skin;

		if (index >= 124 && index <= 135)
			return AssetType.Video;

		if (index >= 136 && index <= 145)
			return AssetType.Visual_effect;

		if (IsTextFile(info))
			return AssetType.Text;

		return AssetType.None;
	}

	private static bool IsTextFile(FileInfo info)
	{
		if (info.Length >= MAX_FILE_SIZE)
			return false;

		char[] fileChars = File.ReadAllText(info.FullName).ToCharArray(0, (int)Mathf.Min(100, info.Length));

		for (int i = 0; i < fileChars.Length; i++)
		{
			if (fileChars[i] >= 128)
				return false;
		}

		return true;
	}

	private static void ExtractClassNames(string guid, string path)
	{
		string scriptText = File.ReadAllText(path);
		string[] frags = scriptText.Split(" class ");

		for (int i = 1; i < frags.Length; i++)
		{
			string prevCheck = frags[i - 1];

			// invalidate "class" after ":"
			if (prevCheck.TrimEnd().Length == 0 || prevCheck.TrimEnd()[^1] == ':')
				continue;

			// long comment check
			string[] commentFrags = prevCheck.Split("/*");

			if (commentFrags.Length > 1 && !commentFrags[^1].Contains("*/"))
				continue;

			// line comment check
			string[] lineFrags = prevCheck.Split('\n');

			if (lineFrags[^1].Contains("//"))
				continue;

			// string check
			string[] sFrag = lineFrags[^1].Split('\"');
			int count = 0;

			if (sFrag.Length > 1)
			{
				for (int j = 0; j < sFrag.Length - 1; j++)
				{
					if (sFrag[j].Length == 0 || sFrag[j][^1] != '\\')
						count++;
				}
			}

			if (count % 2 != 0)
				continue;

			// extract class name
			string className = frags[i].TrimStart().Split(new char[] { ' ', '\'', '\n', '\r', ':', '<' })[0];

			if (!Instance.classToGuids.ContainsKey(className))
				Instance.classToGuids.Add(className, new List<string>());

			if (!Instance.classToGuids[className].Contains(guid))
				Instance.classToGuids[className].Add(guid);

			// check if custom editor
			if (prevCheck.Contains("CustomEditor"))
			{
				string inspectedType = prevCheck.Split('\n', StringSplitOptions.RemoveEmptyEntries)[^2];
				inspectedType = inspectedType.Split(
					new string[] { "CustomEditor", "(", ")", "[", "]", "typeof", "\"" },
					StringSplitOptions.RemoveEmptyEntries
				)[0];

				Instance.classToEditor.Add(inspectedType, className);
			}
		}
	}

	private static void ManageFile(string path, string guid)
	{
		FileInfo file = new FileInfo(path);
		AssetType type = GetAssetType(file);

		// check asset bundle
		foreach (string line in File.ReadAllLines(path + ".meta"))
		{
			if (!line.Contains("assetBundleName: "))
				continue;

			string assetBudleName = line.Split("assetBundleName: ")[1];

			if (assetBudleName.Length > 0)
				AddAssetReference(ASSET_BUNDLE_FLAG + assetBudleName.TrimEnd('\n'), guid);

			break;
		}

		// file specific check
		if (type == AssetType.Scene)
			ManageSceneFile(guid, path, file);

		if (type == AssetType.Assembly)
			ManageJSONFile(guid, path, file);

		if (type == AssetType.Script || type == AssetType.Editor_tool)
			ManageScriptFile(guid, path);

		if (YAML_INDEXES.Contains((int)type) && ((int)type != 9 || file.Extension == UNITY_EXTENSIONS[42]))
			ManageYAMLFile(guid, path, file);
	}

	private static void ManageSceneFile(string guid, string path, FileInfo file)
	{
		// check build settings
		int index = 0;

		while (index < SceneManager.sceneCount)
		{
			if (SceneManager.GetSceneAt(index).path == path)
				AddAssetReference(BUILD_SETTINGS_FLAG, guid);

			index++;
		}

		// check guids in scene
		ManageYAMLFile(guid, path, file);
	}

	private static void ManageJSONFile(string guid, string path, FileInfo file)
	{
		// get refs
		foreach (string line in File.ReadAllLines(path))
		{
			if (!line.Contains(GUID_FLAG.ToUpper()))
				continue;

			string currentGuid = line.Split(GUID_FLAG.ToUpper())[1].Split('\"')[0].Trim();

			if (Instance.results.guidToPath.ContainsKey(currentGuid))
				AddAssetReference(guid, currentGuid);
		}

		// script refs to assembly
		foreach (string scriptPath in GetAllScriptsInAssembly(file, file.Directory))
		{
			string scriptGuid = GetAssetGUID(scriptPath);

			if (Instance.results.guidToPath.ContainsKey(scriptGuid))
			{
				AddAssetReference(scriptGuid, guid);
			}
			else
			{
				Debug.LogError("Script was not detected during indexing or not compiled by Unity, this is a very critical error. Skipping.");
			}
		}
	}

	private static void ManageScriptFile(string guid, string path)
	{
		// list of classes to ignore
		List<string> declaredClasses = new List<string>();

		foreach (KeyValuePair<string, List<string>> pair in Instance.classToGuids)
		{
			if (pair.Value.Contains(guid))
			{
				declaredClasses.Add(pair.Key);

				// link to custom editor
				if (Instance.classToEditor.ContainsKey(pair.Key))
				{
					foreach (string editorGuid in Instance.classToGuids[Instance.classToEditor[pair.Key]])
						AddAssetReference(guid, editorGuid);
				}
			}
		}

		// TODO : Same name classes are getting detected (including subclasses)
		// I'm not sure how I could fix that without making the tool understand C# fully...

		// detect class names
		List<string> scriptLines = new List<string>(File.ReadAllLines(path));
		bool inComment = false;

		for (int i = 0; i < scriptLines.Count; i++)
		{
			string line = scriptLines[i];

			// skip comment line (to go faster)
			if (line.TrimStart().StartsWith("//"))
				continue;

			int startIndex;
			int endIndex;

			if (!inComment)
			{
				// detect if call is valid
				if (line.Contains("Resources.Load"))
				{
					string[] resFrags = line.Split("Resources.Load");
					int stringOpenCount = 0;
					bool inLocalComment = false;
					bool inString = false;

					for (int j = 0; j < resFrags.Length - 1; j++)
					{
						// check inline long comment
						string frag = resFrags[j];
						startIndex = frag.LastIndexOf("/*");
						endIndex = frag.LastIndexOf("*/");

						if (startIndex > endIndex)
							inLocalComment = true;
						else if (endIndex > startIndex)
							inLocalComment = false;

						if (inLocalComment)
							continue;

						// check line comment
						if (frag.Contains("//"))
							break;

						if (frag.Contains('"'))
						{
							string[] sFrags = frag.Split('"');

							for (int h = 0; h < sFrags.Length - 1; h++)
							{
								if (sFrags[h][^1] != '\\')
									stringOpenCount++;
							}

							if (stringOpenCount % 2 != 0)
							{
								inString = true;
								continue;
							}
							else
								inString = false;
						}

						if (!inString)
						{
							for (int h = 1; h < resFrags.Length; h++)
							{
								string resPath = resFrags[h].Split("(")[1].Split(")")[0];

								if (resPath.Contains("\""))
									FindResourcesReference(resPath.Split('"')[1], guid);
							}
						}
					}
				}

				// detect if class name is valid
				foreach (string className in Instance.classToGuids.Keys)
				{
					// skip current declared class
					if (declaredClasses.Contains(className))
						continue;

					if (line.Contains(className))
					{
						string[] frags = line.Split(className);
						int stringOpenCount = 0;
						bool stopNow = false;
						bool inLocalComment = false;
						bool inString = false;

						for (int j = 0; j < frags.Length - 1; j++)
						{
							// check inline long comment
							string frag = frags[j];
							startIndex = frag.LastIndexOf("/*");
							endIndex = frag.LastIndexOf("*/");

							if (startIndex > endIndex)
								inLocalComment = true;
							else if (endIndex > startIndex)
								inLocalComment = false;

							if (inLocalComment)
								continue;

							// check line comment
							if (frags[j].Contains("//"))
							{
								stopNow = true;
								break;
							}

							// check in string
							if (frag.Contains('"'))
							{
								string[] sFrags = frag.Split('"', StringSplitOptions.RemoveEmptyEntries);

								for (int h = 0; h < sFrags.Length - 1; h++)
								{
									if (sFrags[h][^1] != '\\')
										stringOpenCount++;
								}

								if (stringOpenCount % 2 != 0)
								{
									inString = true;
									continue;
								}
								else
									inString = false;
							}

							if (!inString)
							{
								char prevChar = frag[^1];
								char nextChar = frags[j + 1].Length == 0 ? ' ' : frags[j + 1][0];
								bool valid = true;

								if (IsValidInClassName(prevChar) || IsValidInClassName(nextChar))
									valid = false;

								if (valid)
								{
									foreach (string refGuid in Instance.classToGuids[className])
										AddAssetReference(guid, refGuid);
								}
							}
						}

						if (stopNow)
							continue;
					}
				}
			}

			// check long comments
			startIndex = line.LastIndexOf("/*");
			endIndex = line.LastIndexOf("*/");

			if (startIndex > endIndex)
				inComment = true;
			else if (endIndex > startIndex)
				inComment = false;
		}
	}

	private static void ManageYAMLFile(string guid, string path, FileInfo file)
	{
		foreach (string line in File.ReadAllLines(path))
		{
			if (!line.Contains(GUID_FLAG))
				continue;

			string refGuid = line.Split(GUID_FLAG)[1].Split(',')[0].Replace("\\\"", "").Trim();

			if (Instance.results.guidToPath.ContainsKey(refGuid))
				AddAssetReference(guid, refGuid);
		}
	}

	private static void AddAssetReference(string guid, string refGuid)
	{
		// references
		if (!Instance.guidToReferences.ContainsKey(guid))
			Instance.guidToReferences.Add(guid, new List<string>());

		if (!Instance.guidToReferences[guid].Contains(refGuid))
			Instance.guidToReferences[guid].Add(refGuid);

		// sources
		if (!Instance.results.guidToSources.ContainsKey(refGuid))
			Instance.results.guidToSources.Add(refGuid, new List<string>());

		if (!Instance.results.guidToSources[refGuid].Contains(guid))
			Instance.results.guidToSources[refGuid].Add(guid);
	}

	private static void FindResourcesReference(string path, string guid)
	{
		// check resources paths list
		foreach (KeyValuePair<string, string> asset in Instance.resPathToGuid)
		{
			// key = path / value = guid
			string afterResPath = asset.Key.Split("Resources/")[1];
			afterResPath = afterResPath.Replace(new FileInfo(afterResPath).Extension, "");

			if (afterResPath.Replace("\\", "/") == path.Replace("\\", "/"))
			{
				// only ref first match (you shouldn't have multiple res with same path)
				AddAssetReference(guid, asset.Value);
				break;
			}
		}
	}

	private static bool IsValidInClassName(char letter)
	{
		return (letter >= 0 && letter <= 9) || // numbers
			(letter >= 65 && letter <= 90) || // A - Z
			letter == 95 || // _
			(letter >= 97 && letter <= 122); // a - z
	}

	private static List<string> GetAllScriptsInAssembly(FileInfo baseAssembly, DirectoryInfo baseDir)
	{
		List<string> filePaths = new List<string>();

		foreach (FileInfo file in baseDir.GetFiles())
		{
			// we cut here
			if (file.Extension == UNITY_EXTENSIONS[24] && file != baseAssembly)
				return new List<string>();

			// add script
			if (file.Extension == UNITY_EXTENSIONS[85])
				filePaths.Add(ConvertToProjectPath(file.FullName));
		}

		foreach (DirectoryInfo directory in baseDir.GetDirectories())
			filePaths.AddRange(GetAllScriptsInAssembly(baseAssembly, directory));

		return filePaths;
	}

	private static string ConvertToProjectPath(string fullPath)
	{
		return fullPath.Replace(Application.dataPath.Replace("/", "\\"), "Assets");
	}

	private string CleanAssetType(AssetType type) => type.ToString().Replace('_', ' ');

	private void GenerateIfNeeded()
	{
		if (titleStyle == null)
			titleStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontStyle = FontStyle.Bold };

		if (rightStyle == null)
			rightStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleRight };

		if (textStyle == null)
		{
			textStyle = new GUIStyle(GUI.skin.label)
			{
				alignment = TextAnchor.MiddleLeft,
				richText = true,
				wordWrap = true
			};
		}

		if (buttonStyle == null)
			buttonStyle = new GUIStyle(GUI.skin.button) { alignment = TextAnchor.MiddleLeft };

		if (centerStyle == null)
			centerStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter };
	}

	private void CenterDisplay(Action callback)
	{
		EditorGUILayout.BeginHorizontal();
		{
			EditorGUILayout.Space();
			callback?.Invoke();
			EditorGUILayout.Space();
		}
		EditorGUILayout.EndHorizontal();
	}

	private void GUIDivider() => EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);

	private static void CancelOperations()
	{
		if (Instance == null)
			return;

		Instance.cancelFlag = true;
		Instance.status = AnalysisStatus.Before;
	}

	private static bool CheckInterruption()
	{
		if (Instance == null)
		{
			Instance.status = AnalysisStatus.Before;
			EditorApplication.update -= AsyncAssetIndexing;
			EditorApplication.update -= AsyncAnalysis;
			EditorApplication.update -= AsyncRefAnalysis;
		}

		if (Instance.cancelFlag)
			CancelOperations();

		return Instance.cancelFlag || Instance == null;
	}

	private void DisplayColored(Action callback, Color color)
	{
		GUI.color = color;
		callback?.Invoke();
		GUI.color = Color.white;
	}

	private static void DereferenceAsset(string guid)
	{
		if (Instance.results.guidToPath.ContainsKey(guid))
			Instance.results.guidToPath.Remove(guid);

		if (Instance.results.guidToSources.ContainsKey(guid))
		{
			Instance.results.guidToSources.Remove(guid);

			foreach (KeyValuePair<string, List<string>> asset in Instance.results.guidToSources)
			{
				if (asset.Value.Contains(guid))
					asset.Value.Remove(guid);
			}
		}

		if (Instance.results.guidToRefStatus.ContainsKey(guid))
			Instance.results.guidToRefStatus.Remove(guid);

		if (Instance.classToGuids != null)
		{
			foreach (KeyValuePair<string, List<string>> asset in Instance.classToGuids)
			{
				if (asset.Value.Contains(guid))
					asset.Value.Remove(guid);
			}
		}

		if (Instance.guidToReferences != null)
		{
			if (Instance.guidToReferences.ContainsKey(guid))
				Instance.guidToReferences.Remove(guid);

			foreach (KeyValuePair<string, List<string>> asset in Instance.guidToReferences)
			{
				if (asset.Value.Contains(guid))
					asset.Value.Remove(guid);
			}
		}

		List<KeyValuePair<string, string>> selected = Instance.selectedRefResults.FindAll(x => x.Key == guid);
		selected.ForEach(x => Instance.selectedRefResults.Remove(x));
		selected.ForEach(x => Instance.selectedSearchResults.Remove(x));
		selected.ForEach(x => Instance.selectedSortResults.Remove(x));
		selected.ForEach(x => Instance.selectedPageResults.Remove(x));

		if (Instance.guidToFoldoutStatus.ContainsKey(guid))
			Instance.guidToFoldoutStatus.Remove(guid);

		if (Instance.guidToSelectedStatus.ContainsKey(guid))
			Instance.guidToSelectedStatus.Remove(guid);
	}

	private static void MoveAsset(string originPath, string newPath)
	{
		if (!File.Exists(originPath))
		{
			Debug.LogWarning("Couldn't find the asset you're trying to remove. You probably need to run a new project analysis. Skipping file.");
			return;
		}

		string path = originPath.Replace("\\", "/").Replace(Application.dataPath.Replace("\\", "/"), "");
		List<string> dirs = new List<string>(path.Split("/", StringSplitOptions.RemoveEmptyEntries));
		string currentDir = null;
		bool isRecovery = dirs.Contains(RECOVERY_DIR);

		if (isRecovery)
			dirs.RemoveAt(0);

		for (int i = 0; i < dirs.Count - 1; i++)
		{
			if (currentDir != null)
				currentDir = Path.Combine(currentDir, dirs[i]);
			else
				currentDir = dirs[i];

			string fullPath = Path.Combine(isRecovery ? Application.dataPath : recoveryDir, currentDir);

			if (!Directory.Exists(Path.GetFullPath(fullPath)))
				Directory.CreateDirectory(Path.GetFullPath(fullPath));
		}

		if (File.Exists(newPath))
			File.Delete(newPath);

		File.Move(originPath, newPath);
	}

	private List<string> GetFilesRecursive(string sourceDir)
	{
		List<string> files = new List<string>(Directory.GetFiles(sourceDir));

		foreach (string dir in Directory.GetDirectories(sourceDir))
			files.AddRange(GetFilesRecursive(dir));

		return files;
	}

	private static void GenerateSelectionDictionary()
	{
		Instance.guidToSelectedStatus = new Dictionary<string, bool>();
		Instance.guidToFoldoutStatus = new Dictionary<string, bool>();

		foreach (KeyValuePair<string, string> pair in Instance.results.guidToPath)
		{
			Instance.guidToSelectedStatus.Add(pair.Key, false);
			Instance.guidToFoldoutStatus.Add(pair.Key, false);
		}
	}

	private static void RegisterAssetRef(List<string> list, string guid)
	{
		Instance.results.guidToRefStatus[guid] = true;

		if (Instance.guidToReferences.ContainsKey(guid))
		{
			Instance.guidToReferences[guid].ForEach(item =>
			{
				if (!list.Contains(item))
					list.Add(item);
			});
		}
	}

	private void AsyncLoadingBar(string title, float progress)
	{
		EditorGUILayout.LabelField(title, centerStyle);
		EditorGUILayout.Space();

		Rect rect = EditorGUILayout.BeginVertical();
		EditorGUI.ProgressBar(rect, progress, title);
		EditorGUILayout.EndVertical();
	}

	/// <summary>Save class for results of the Unused Asset Finder</summary>
	[Serializable]
	private class AnalysisResults
	{
		// usable part
		public Dictionary<string, string> guidToPath;
		public Dictionary<string, List<string>> guidToSources;
		public Dictionary<string, bool> guidToRefStatus;

		// serialization part
		[SerializeField]
		private List<string> guidToPathKeys;
		[SerializeField]
		private List<string> guidToPathValues;

		[SerializeField]
		private List<string> guidToSourcesKeys;
		[SerializeField]
		private List<Values> guidToSourcesValues;

		[SerializeField]
		private List<string> guidToRefStatusKeys;
		[SerializeField]
		private List<bool> guidToRefStatusValues;

		public AnalysisResults()
		{
			guidToPath = new Dictionary<string, string>();
			guidToSources = new Dictionary<string, List<string>>();
			guidToRefStatus = new Dictionary<string, bool>();
		}

		public void PreSerialization()
		{
			guidToPathKeys = new List<string>();
			guidToPathValues = new List<string>();

			foreach (KeyValuePair<string, string> pair in guidToPath)
			{
				guidToPathKeys.Add(pair.Key);
				guidToPathValues.Add(pair.Value);
			}

			guidToSourcesKeys = new List<string>();
			guidToSourcesValues = new List<Values>();

			foreach (KeyValuePair<string, List<string>> pair in guidToSources)
			{
				guidToSourcesKeys.Add(pair.Key);
				guidToSourcesValues.Add(new Values(pair.Value));
			}

			guidToRefStatusKeys = new List<string>();
			guidToRefStatusValues = new List<bool>();

			foreach (KeyValuePair<string, bool> pair in guidToRefStatus)
			{
				guidToRefStatusKeys.Add(pair.Key);
				guidToRefStatusValues.Add(pair.Value);
			}
		}

		public void PostSerialization()
		{
			guidToPath = new Dictionary<string, string>();

			for (int i = 0; i < guidToPathKeys.Count; i++)
				guidToPath.Add(guidToPathKeys[i], guidToPathValues[i]);

			guidToSources = new Dictionary<string, List<string>>();

			for (int i = 0; i < guidToSourcesKeys.Count; i++)
			{
				guidToSources.Add(guidToSourcesKeys[i], guidToSourcesValues[i].data);
			}

			guidToRefStatus = new Dictionary<string, bool>();

			for (int i = 0; i < guidToRefStatusKeys.Count; i++)
				guidToRefStatus.Add(guidToRefStatusKeys[i], guidToRefStatusValues[i]);
		}

		[Serializable]
		public class Values
		{
			public List<string> data;

			public Values(List<string> data) => this.data = data;
		}
	}

	/// <summary>Preview popup for the Unused Assets Finder</summary>
	private class PreviewPopup : EditorWindow
	{
		private const float minPadding = 10;

		private static PreviewPopup _instance;
		private static PreviewPopup Instance
		{
			get
			{
				if (_instance == null)
					SpawnWindowIfNeeded();

				return _instance;
			}
		}

		private GUIStyle textStyle;

		private System.Object obj;
		private AssetType type;
		private string path;
		private Texture display;
		private Vector2 scroll;
		private Editor SO_Editor;

		// Main flow methods
		private static void SpawnWindowIfNeeded()
		{
			bool needInit = false;

			if (_instance == null)
				needInit = true;

			_instance = GetWindow<PreviewPopup>();

			if (needInit)
			{
				_instance.titleContent = new GUIContent("Unused Assets Finder : Preview");
				_instance.minSize = new Vector2(250, 250);
			}
		}

		public static void Preview(string path, AssetType type)
		{
			SpawnWindowIfNeeded();

			Instance.obj = AssetDatabase.LoadAssetAtPath(path, GetType(type));
			Instance.type = type;
			Instance.path = path;

			Instance.display = null;
			Instance.SO_Editor = null;
		}

		private void OnGUI()
		{
			GenerateIfNeeded();

			if (display == null)
			{
				switch (type)
				{
					case AssetType.Texture:
						display = obj as Texture;
						break;

					case AssetType.Script:
					case AssetType.Editor_tool:
					case AssetType.Shader:
					case AssetType.Text:
						if (type == AssetType.Shader && new FileInfo(path).Extension == UNITY_EXTENSIONS[95])
							EditorGUILayout.HelpBox("No preview for " + type + " assets.", MessageType.Warning);
						else
						{
							scroll = EditorGUILayout.BeginScrollView(scroll);
							EditorGUILayout.LabelField(File.ReadAllText(path), textStyle);
							EditorGUILayout.EndScrollView();
						}
						break;

					case AssetType.Prefab:
					case AssetType.Material:
						display = AssetPreview.GetAssetPreview(obj as UnityEngine.Object);
						break;

					case AssetType._3D_model:
						GameObject gameObj = obj as GameObject;
						MeshFilter filter = gameObj.GetComponent<MeshFilter>();
						SkinnedMeshRenderer skin = gameObj.GetComponent<SkinnedMeshRenderer>();

						if (filter != null || skin != null)
						{
							Mesh mesh = filter != null ? filter.sharedMesh : skin.sharedMesh;
							display = AssetPreview.GetAssetPreview(mesh);
						}
						else
							EditorGUILayout.HelpBox("No mesh found in this asset.", MessageType.Error);
						break;

					case AssetType.Scriptable_object:
						if (SO_Editor == null)
							SO_Editor = Editor.CreateEditor(obj as ScriptableObject);

						if (SO_Editor != null)
						{
							scroll = EditorGUILayout.BeginScrollView(scroll);
							SO_Editor.OnInspectorGUI();
							EditorGUILayout.EndScrollView();
						}
						else
							EditorGUILayout.HelpBox("No preview for " + type + " assets.", MessageType.Warning);

						break;

					default:
						EditorGUILayout.HelpBox("No preview for " + type + " assets.", MessageType.Warning);
						break;
				}
			}
			else
				RenderTexture(display);
		}

		// Utility methods
		private static Type GetType(AssetType assetType)
		{
			switch (assetType)
			{
				case AssetType.Texture:
					return typeof(Texture);

				case AssetType.Prefab:
					return typeof(GameObject);

				case AssetType.Material:
					return typeof(Material);

				case AssetType._3D_model:
					return typeof(GameObject);

				case AssetType.Scriptable_object:
					return typeof(ScriptableObject);

				default:
					return null;
			}
		}

		private void GenerateIfNeeded()
		{
			if (textStyle == null)
				textStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.UpperLeft, wordWrap = true };
		}

		private void RenderTexture(Texture texture)
		{
			if (texture == null)
			{
				EditorGUILayout.HelpBox("Couldn't render asset.", MessageType.Error);
				return;
			}

			EditorGUI.DrawPreviewTexture(GetDisplayRect(), texture);
		}

		private Rect GetDisplayRect()
		{
			float minSize = Mathf.Min(Instance.position.width, Instance.position.height);
			float sizeDif = Instance.position.width - Instance.position.height;
			float verticalPadding = sizeDif < 0 ? -sizeDif / 2 : 0;
			float horizontalPadding = sizeDif > 0 ? sizeDif / 2 : 0;

			return new Rect(
				new Vector2(minPadding + horizontalPadding, minPadding + verticalPadding),
				Vector2.one * (minSize - minPadding * 2)
			);
		}
	}
}