using System;
using System.IO;
using GlitchyEngine.Collections;
using System.Collections;
using GlitchyEngine.Renderer;
using System.Threading;
using GlitchyEngine.Content;
using GlitchyEditor.Assets;
using GlitchyEngine;
using System.Linq;

namespace GlitchyEditor;

/*class PreviewImageManager
{
	/// Thread that loads/generates preview images in the background
	//private Thread _loaderThread;

	/// Set to true to notify the loader to stop
	//private bool _stopLoader;

	private append Dictionary<String, Texture2D> _previewImages = .() ~ {
		for (var v in _)
		{
			delete v.key;
			v.value.ReleaseRef();
		}

		delete _;
	};

	public Texture2D GetPreviewImage(String assetName)
	{
		if (_previewImages.TryGetValue(assetName, let image))
		{
			return image..AddRef();
		}

		if (assetName.EndsWith(".png", .OrdinalIgnoreCase))
		{
			/*using (Texture2D texture = new Texture2D(assetName, true))
			{
				Texture2DDesc desc = .(128, 128, .R8G8B8A8_UNorm_SRGB)
					{
						Usage = .Immutable
					};

				Texture2D myActualTexture = new Texture2D(desc);

				// TODO: Scale down texture (on GPU?)
			}*/

			Texture2D texture = new Texture2D(assetName, true);

			if (texture.Width <= 128 && texture.Height <= 128)
			{

			}

			if ( texture.MipLevels > 1)
			{

			}

			_previewImages.Add(new String(assetName), texture);

			return texture..AddRef();
		}
		else
		{
			Runtime.NotImplemented();
		}
	}
}*/

public class AssetNode
{
	public String Name ~ delete _;
	public String Path ~ delete _;

	public bool IsDirectory;

	public AssetFile AssetFile ~ delete _;

	public List<SubAsset> SubAssets ~ {
		SubAssets?.ClearAndDeleteItems();
		delete SubAssets;
	}

	public Texture2D PreviewImage ~ _?.ReleaseRef();
}

public class SubAsset
{
	public AssetNode Asset;
	public String Name ~ delete _;
	//public String AssetInternalPath ~ delete _;

	public Texture2D PreviewImage ~ _?.ReleaseRef();
}

class AssetHierarchy
{
	FileSystemWatcher fsw ~ {
		_.StopRaisingEvents();
		delete _;
	};
	
	bool _fileSystemDirty = false;
	
	internal TreeNode<AssetNode> _assetHierarchy = null ~ DeleteTreeAndChildren!(_);
	private append Dictionary<StringView, TreeNode<AssetNode>> _pathToAssetNode = .();
	
	private append String _contentDirectory = .();

	private EditorContentManager _contentManager;

	public StringView ContentDirectory
	{
		get => _contentDirectory;
		private set
		{
			_contentDirectory.Clear();
			_contentDirectory.Append(value);
			Path.Fixup(_contentDirectory);
		}
	}

	public this(EditorContentManager contentManager)
	{
		_contentManager = contentManager;
	}

	public void SetContentDirectory(StringView contentDirectory)
	{
		ContentDirectory = contentDirectory;

		_fileSystemDirty = true;

		SetupFileSystemWatcher();

		Update();
	}
	
	/// Initializes the FSW for the current ContentDirectory and registers the events.
	private void SetupFileSystemWatcher()
	{
		delete fsw;
		fsw = new FileSystemWatcher(_contentDirectory);
		fsw.IncludeSubdirectories = true;

		fsw.OnChanged.Add(new (filename) => {
			// Note: Gets fired for a directory if a file inside it is created/removed
			
			Log.EngineLogger.Trace($"File content changed (\"{filename}\")");
			//_fileSystemDirty = true;

			FileContentChanged(filename);
		});

		fsw.OnCreated.Add(new (filename) => {
			Log.EngineLogger.Trace($"File created (\"{filename}\")");

			_fileSystemDirty = true;
		});

		fsw.OnDeleted.Add(new (filename) => {
			Log.EngineLogger.Trace($"File deleted (\"{filename}\")");

			_fileSystemDirty = true;
		});

		fsw.OnRenamed.Add(new (oldName, newName) => {
			Log.EngineLogger.Trace($"File renamed (From \"{oldName}\" to \"{newName}\")");
			
			_fileSystemDirty = true;
			/*String contentFilePath = scope String();

			Path.InternalCombine(contentFilePath, ContentDirectory, oldName);

			//_fileSystemDirty = true;
			TreeNode<AssetNode> fileNode = GetNodeFromPath(contentFilePath);
			fileNode->*/
		});

		fsw.StartRaisingEvents();
	}

	/// Gets the tree node for the given filePath or null, if the file/directory doesn't exist.
	/// @param filePath the path for which to return the tree node.
	/// @remarks Do not hold a reference to the TreeNode because it can become invalid when the file hierarchy changes.
	public Result<TreeNode<AssetNode>> GetNodeFromPath(StringView filePath)
	{
		if (_pathToAssetNode.TryGetValue(filePath, let treeNode))
		{
			return treeNode;
		}

		return .Err;
	}

	public bool FileExists(StringView filePath)
	{
		return _pathToAssetNode.ContainsKey(filePath);
	}

	public void Update()
	{
		// TODO: do we really need to do this in the update loop?
		if (_fileSystemDirty)
		{
			UpdateFiles();
		}
	}
	

	/// Rebuilds the asset file hierarchy.
	private void UpdateFiles()
	{
		Log.EngineLogger.Trace($"Updating asset hierarchy");

		if (_assetHierarchy == null)
		{
			// TODO: move to init?

			_assetHierarchy = new TreeNode<AssetNode>(new AssetNode());
			_assetHierarchy->Path = new String(ContentDirectory);
			_assetHierarchy->Name = new String("Content");

			Log.EngineLogger.Trace($"Created directory node for: \"{_assetHierarchy->Path}\"");

			_pathToAssetNode.Add(ContentDirectory, _assetHierarchy);
		}

		void HandleFile(AssetNode node)
		{
			String identifier = scope .(node.Path.Length);
			Path.GetRelativePath(node.Path, _contentDirectory, identifier);

			node.AssetFile = new AssetFile(_contentManager, identifier, node.Path, node.IsDirectory);
		}

		/// Determines the files that belong to the given directory and adds them to the tree.
		void AddFilesOfDirectory(TreeNode<AssetNode> directory)
		{
			// Filter that accepts all files.
			String filter = scope $"{directory->Path}/*";

			// Buffer used to hold the path of the files iterated below.
			String filepathBuffer = scope String(256);
			// Buffer used to hold the file extension of the files iterated below.
			String extensionBuffer = scope String(16);

			for (var entry in Directory.Enumerate(filter, .Files))
			{
				entry.GetFilePath(filepathBuffer..Clear());

				Path.GetExtension(filepathBuffer, .. extensionBuffer..Clear());

				// Ignore meta files.
				if (extensionBuffer.Equals(AssetFile.ConfigFileExtension, .OrdinalIgnoreCase))
					continue;

				TreeNode<AssetNode> treeNode = directory.Children.Where(scope (node) => node.Value.Path == filepathBuffer).FirstOrDefault();

				if (treeNode == null)
				{
					AssetNode assetNode = new AssetNode();
					assetNode.Name = new String();
					Path.GetFileName(filepathBuffer, assetNode.Name);
					
					filepathBuffer.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

					assetNode.Path = new String(filepathBuffer);
					assetNode.IsDirectory = false;

					treeNode = directory.AddChild(assetNode);
					_pathToAssetNode.Add(assetNode.Path, treeNode);

					//GrabSubAssets(node);
					HandleFile(treeNode.Value);

					Log.EngineLogger.Trace($"Created file node for: \"{assetNode.Path}\"");
				}
			}
		}

		void RemoveOrphanedEntries(TreeNode<AssetNode> node)
		{
			/// Removes the node and its children from _pathToAssetNode
			void RemoveSubtree(TreeNode<AssetNode> tree)
			{
				_pathToAssetNode.Remove(tree->Path);

				for (var child in tree.Children)
				{
					RemoveSubtree(child);
				}
			}

			for (TreeNode<AssetNode> child in node.Children)
			{
				if (!Directory.Exists(child->Path) && !File.Exists(child->Path))
				{
					Log.EngineLogger.Trace($"Removed orphaned node for: \"{child->Path}\"");

					@child.Remove();

					RemoveSubtree(child);

					DeleteTreeAndChildren!(child);
				}
			}
		}
		
		/// Adds the given directory to the specified tree.
		/// Recursively adds all Files and Subdirectories.
		void AddDirectoryToTree(String path, TreeNode<AssetNode> parentNode)
		{
			path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

			// Try to find the node for the specified path in the given parent
			TreeNode<AssetNode> treeNode = parentNode.Children.Where(scope (node) => node.Value.Path == path).FirstOrDefault();

			// Create new Node for the Directory, if no TreeNode exists.
			if (treeNode == null)
			{
				AssetNode assetNode = new AssetNode();
				assetNode.Path = new String(path);
				assetNode.Name = new String();
				assetNode.IsDirectory = true;
				Path.GetFileName(assetNode.Path, assetNode.Name);

				treeNode = parentNode.AddChild(assetNode);
				_pathToAssetNode.Add(assetNode.Path, treeNode);

				Log.EngineLogger.Trace($"Created directory node for: \"{assetNode.Path}\"");
			}

			String directoryNameBuffer = scope String(256);

			// Filter that finds all entries of a directory.
			String filter = scope $"{path}/*";

			for (var directory in Directory.Enumerate(filter, .Directories))
			{
				directory.GetFilePath(directoryNameBuffer..Clear());

				AddDirectoryToTree(directoryNameBuffer, treeNode);
			}

			AddFilesOfDirectory(treeNode);

			RemoveOrphanedEntries(treeNode);
		}
		
		String filter = scope $"{ContentDirectory}/*";
		
		String directoryNameBuffer = scope String(256);

		for (var directory in Directory.Enumerate(filter, .Directories))
		{
			directory.GetFilePath(directoryNameBuffer..Clear());

			AddDirectoryToTree(directoryNameBuffer, _assetHierarchy);
		}
		
		RemoveOrphanedEntries(_assetHierarchy);

		_fileSystemDirty = false;
	}

	private void FileContentChanged(StringView fileName)
	{
		var fileName;

		// Config files aren't really tracked but changing them effectively changes the corresponding file
		// so we fire the event for them.
		if (fileName.EndsWith(AssetFile.ConfigFileExtension))
			fileName.RemoveFromEnd(AssetFile.ConfigFileExtension.Length);

		String fileNameWithContentRoot = scope .();
		Path.InternalCombine(fileNameWithContentRoot, _contentDirectory, fileName);

		var nodeResult = GetNodeFromPath(fileNameWithContentRoot);

		TreeNode<AssetNode> node = null;

		if (!(nodeResult case .Ok(out node)))
		{
			// This happens, when we create new files.
			Log.EngineLogger.Trace($"Could not find node for file \"{fileNameWithContentRoot}\"");
			return;
		}

		// Don't fire event for directories.
		if (node->IsDirectory)
			return;

		OnFileContentChanged(node.Value);

		// TODO: Handle file changes (reload asset, etc...)
	}

	public delegate void FileContentChangedFunc(AssetNode node);

	public Event<FileContentChangedFunc> OnFileContentChanged ~ _.Dispose();
}

class EditorContentManager : IContentManager
{
	private append String _contentDirectory = .();

	public StringView ContentDirectory => _contentDirectory;
	
	//private append List<String> _identifiers = .() ~ _.ClearAndDeleteItems();

	private append Dictionary<StringView, Asset> _loadedAssets = .(); // Check if all resources are unloaded
	
	private append AssetHierarchy _assetHierarchy = .(this);

	public AssetHierarchy AssetHierarchy => _assetHierarchy;

	public this()
	{
		_assetHierarchy.OnFileContentChanged.Add(new => OnFileContentChanged);
	}

	public ~this()
	{
		UnmanageAllAssets();
	}

	private void OnFileContentChanged(AssetNode assetNode)
	{
		// TODO: Subassets break reloading because we can't find them when we only receive the file that changed...

		// Asset isn't loaded so we don't need to reload it.
		if (assetNode.AssetFile.LoadedAsset == null)
			return;

		String neededAssetLoaderName = assetNode.AssetFile.AssetConfig?.AssetLoader;

		if (String.IsNullOrWhiteSpace(neededAssetLoaderName))
			return;

		IAssetLoader assetLoader = null;

		String loaderNameBuffer = scope String(64);

		for (IAssetLoader loader in _assetLoaders)
		{
			loader.GetType().GetName(loaderNameBuffer..Clear());

			if (loaderNameBuffer == neededAssetLoaderName)
			{
				assetLoader = loader;
				break;
			}
		}

		if (assetLoader == null)
		{
			Log.EngineLogger.Error($"Could not find asset loader \"{neededAssetLoaderName}\"");
			return;
		}

		if (var assetReloader = assetLoader as IReloadingAssetLoader)
		{
			Stream stream = GetStream(assetNode.Path);

			assetReloader.ReloadAsset(assetNode.AssetFile, stream);

			delete stream;
		}
	}

	public void SetContentDirectory(StringView contentDirectory)
	{
		_contentDirectory.Clear();
		_contentDirectory.Append(contentDirectory);
		Path.Fixup(_contentDirectory);

		_assetHierarchy.SetContentDirectory(contentDirectory);
	}

	public void Update()
	{
		_assetHierarchy.Update();
	}

	public IAssetLoader GetDefaultAssetLoader(StringView fileExtension)
	{
		if (_defaultAssetLoaders.TryGetValue(fileExtension, let value))
			return value;

		return null;
	}
	private append List<String> _supportedExtensions = .() ~ ClearAndDeleteItems!(_);
	private append List<IAssetLoader> _assetLoaders = .() ~ ClearAndDeleteItems!(_);
	private append Dictionary<StringView, IAssetLoader> _defaultAssetLoaders = .();
	private append Dictionary<String, function AssetPropertiesEditor(AssetFile)> _assetPropertiesEditors = .() ~ {
		for (String key in _.Keys)
		{
			delete key;
		}
	};

	public void RegisterAssetLoader<T>() where T : new, class, IAssetLoader
	{
		//Log.EngineLogger.AssertDebug(!_assetLoaders.Any((l) => l.GetType() == typeof(T)), "Asset loader already registered.");

		_assetLoaders.Add(new T());

		for (StringView ext in T.FileExtensions)
			_supportedExtensions.Add(new String(ext));
	}

	public void SetAsDefaultAssetLoader<T>(params Span<StringView> fileExtensions) where T : IAssetLoader
	{
		for (var ext in fileExtensions)
		{
			// Find file extension in registered file extensions
			String foundExtension = null;

			for (var supportedExt in _supportedExtensions)
			{
				if (supportedExt == ext)
				{
					foundExtension = supportedExt;
					break;
				}
			}

			Log.EngineLogger.Assert(foundExtension != null, "File Extension is not registered.");
			
			for (var loader in _assetLoaders)
			{
				if (loader.GetType() == typeof(T))
				{
					_defaultAssetLoaders[foundExtension] = loader;
					break;
				}
			}
		}
	}
	
	public void SetAssetPropertiesEditor(Type assetLoaderType, function AssetPropertiesEditor(AssetFile) editorFactory)
	{
		String loaderTypeName = new String();
		assetLoaderType.GetName(loaderTypeName);

		_assetPropertiesEditors[loaderTypeName] = editorFactory;
	}

	public void SetAssetPropertiesEditor<TAssetLoader>(function AssetPropertiesEditor(AssetFile) editorFactory) where TAssetLoader : IAssetLoader
	{
		SetAssetPropertiesEditor(typeof(TAssetLoader), editorFactory);
	}

	public AssetPropertiesEditor GetNewPropertiesEditor(AssetFile assetFile)
	{
		if (assetFile?.AssetConfig.AssetLoader == null)
			return null;

		if (_assetPropertiesEditors.TryGetValue(assetFile.AssetConfig.AssetLoader, let propertiesEditorfactory))
			return propertiesEditorfactory(assetFile);

		return null;
	}

	public bool IsLoaded(StringView identifier)
	{
		return _loadedAssets.ContainsKey(identifier);
	}

	public Asset LoadAsset(StringView identifier)
	{
		if (_loadedAssets.TryGetValue(identifier, let asset))
		{
			return asset..AddRef();
		}

		// Find subasset name
		int poundIndex = identifier.IndexOf('#');

		StringView resourceName = poundIndex == -1 ? identifier : identifier.Substring(0, poundIndex);
		StringView? subassetName = identifier.Substring(poundIndex + 1);

		String filePath = scope String(resourceName.Length + _contentDirectory.Length + 2);
		Path.Combine(filePath, _contentDirectory, resourceName);

		Path.Fixup(filePath);

		//filePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

		Result<TreeNode<AssetNode>> resultNode = AssetHierarchy.GetNodeFromPath(filePath);

		if (resultNode case .Err)
		{
			Log.EngineLogger.Error($"Could not find asset \"{filePath}\".");
			return null;
		}

		//AssetFile file = scope .(this, filePath, false);
		AssetFile file = resultNode->Value.AssetFile;
		
		IAssetLoader assetLoader = null;

		String loaderTypeName = scope .(128);

		for (IAssetLoader loader in _assetLoaders)
		{
			loader.GetType().GetName(loaderTypeName..Clear());

			if (loaderTypeName == file.AssetConfig.AssetLoader)
			{
				assetLoader = loader;
				break;
			}
		}

		Log.EngineLogger.AssertDebug(assetLoader != null);

		Stream stream = GetStream(filePath);

		Asset loadedAsset = assetLoader.LoadAsset(stream, file.AssetConfig.Config, resourceName, subassetName, this);
		
		delete stream;

		//String identifierString = new .(identifier);
		//_identifiers.Add(identifierString);

		//_loadedAssets[identifierString] = loadedAsset;
		

		loadedAsset.Identifier = identifier;
		ManageAsset(loadedAsset);

		file.[Friend]_loadedAsset = loadedAsset;

		return loadedAsset;
	}

	// TODO: probably not needed
	public Stream GetStream(StringView assetIdentifier)
	{
		var assetIdentifier;

		if (!assetIdentifier.StartsWith(_contentDirectory))
		{
			String filePath = scope:: String(assetIdentifier.Length + _contentDirectory.Length + 2);
			Path.Combine(filePath, _contentDirectory, assetIdentifier);

			assetIdentifier = filePath;
		}

		FileStream fs = new FileStream();
		var result = fs.Open(assetIdentifier, .Open, .Read, .ReadWrite);

		if (result case .Err)
			return null;

		return fs;
	}

	public void ManageAsset(Asset asset)
	{
		_loadedAssets.Add(asset.Identifier, asset);
		asset.[Friend]_contentManager = this;
	}

	public void UnmanageAsset(Asset asset)
	{
		_loadedAssets.Remove(asset.Identifier);
		asset.[Friend]_contentManager = null;
	}

	/// This will unregister all assets from this content manager.
	/// Note: This will not release any assets.
	private void UnmanageAllAssets()
	{
		for (let (_, asset) in _loadedAssets)
		{
			UnmanageAsset(asset);
		}
	}

	public void UpdateAssetIdentifier(Asset asset, StringView oldIdentifier, StringView newIdentifier)
	{
		if (oldIdentifier == newIdentifier)
			return;

		Log.EngineLogger.Assert(_loadedAssets.ContainsKey(newIdentifier), "An asset with the same identifier is already managed by this content manager.");

		// Since all we do in order to track assets is add them to a dictionary we can simply unmanage and manage it again.
		UnmanageAsset(asset);
		ManageAsset(asset);
	}
}