using GlitchyEngine;
using GlitchyEngine.Collections;
using GlitchyEngine.Renderer;
using System;
using System.Collections;
using System.IO;
using System.Linq;
using GlitchyEngine.Content;
using GlitchyEditor.Multithreading;

namespace GlitchyEditor.Assets;

class AssetHierarchy
{
	FileSystemWatcher fsw ~ {
		_?.StopRaisingEvents();
		delete _;
	};
	
	internal TreeNode<AssetNode> _assetRootNode = null ~ DeleteTreeAndChildren!(_);
	internal TreeNode<AssetNode> _resourcesDirectoryNode = null;
	internal TreeNode<AssetNode> _assetsDirectoryNode = null;
	private append Dictionary<StringView, TreeNode<AssetNode>> _pathToAssetNode = .();
	private append Dictionary<StringView, TreeNode<AssetNode>> _identifierToAssetNode = .();
	private append Dictionary<AssetHandle, TreeNode<AssetNode>> _handleToAssetNode = .();
	
	private append String _resourcesDirectory = .();
	private append String _assetsDirectory = .();

	private const float FileSystemDebounceSeconds = 0.5f;

	private EditorContentManager _contentManager;

	public TreeNode<AssetNode> RootNode => _assetRootNode;

	public StringView ResourcesDirectory
	{
		get => _resourcesDirectory;
		private set
		{
			_resourcesDirectory.Set(value);
			Path.Fixup(_resourcesDirectory);
		}
	}

	public StringView AssetsDirectory
	{
		get => _assetsDirectory;
		private set
		{
			_assetsDirectory.Set(value);
			Path.Fixup(_assetsDirectory);
		}
	}

	public this(EditorContentManager contentManager)
	{
		_contentManager = contentManager;

		// Create a root node that will contain all Asset directory nodes.
		_assetRootNode = new TreeNode<AssetNode>(new AssetNode());
		_assetRootNode->Path = new String();
		_assetRootNode->Name = new String();
		_assetRootNode->IsDirectory = true;

		Log.EngineLogger.Trace($"Created root node");
	}

	public void DeletePathsBackground(Span<StringView> paths)
	{
		DeleteBackgroundTask deleteTask = new .(paths);
		deleteTask.DeleteWhenEnded = true;

		for (StringView path in paths)
		{
			Result<TreeNode<AssetNode>> nodeResult = GetNodeFromPath(path);

			if (nodeResult case .Ok(TreeNode<AssetNode> assetNode))
			{
				StringView assetDescriptorPath = assetNode->AssetFile?.AssetConfigPath ?? "";
	
				// If it exists, also try to delete the .ass file
				if (File.Exists(assetDescriptorPath))
				{
					deleteTask.AddPath(assetDescriptorPath);
				}
			}
		}

		EditorApp.Instance.BackgroundTaskManager.StartBackgroundTask(deleteTask);
	}

	/// Deletes the file that belongs to the given assetNode
	public void DeleteFile(AssetNode assetNode)
	{
		if (assetNode.IsDirectory)
		{
			if (Directory.DelTree(assetNode.Path) case .Err(let error))
			{
				Log.EngineLogger.Error($"Couldn't delete Directory ({error}).");
			}
		}
		else
		{
			if (File.Delete(assetNode.Path) case .Err(let error))
			{
				Log.EngineLogger.Error($"Couldn't delete File \"{assetNode.Path}\" ({error}).");
			}
		}

		StringView assetDescriptorPath = assetNode.AssetFile?.AssetConfigPath ?? "";

		// If it exists, also try to delete the .ass file
		if (File.Exists(assetDescriptorPath))
		{
			if (File.Delete(assetDescriptorPath) case .Err(let error))
			{
				Log.EngineLogger.Error($"Couldn't delete Asset descriptor file \"{assetDescriptorPath}\" ({error}).");
			}
		}
	}

	/// Renames the given assetNode to the specified name.
	public void RenameFile(AssetNode assetNode, StringView fileName)
	{
		String directory = scope .();
		if (Path.GetDirectoryPath(assetNode.Path, directory) case .Err)
		{
			Log.EngineLogger.Error($"Rename failed. Couldn't get directory for path \"{assetNode.Path}\".");
			return;
		}

		String newName = scope .();
		Path.Combine(newName, directory, fileName);

		if (File.Move(assetNode.Path, newName) case .Err(let error))
		{
			Log.EngineLogger.Error($"Couldn't rename file from {assetNode.Path} to {newName} ({error}).");
		}

		// We don't need to rename the .ass file, because the filesystem watcher will handle this for us!
	}

	public Result<void> MoveFileToNode(StringView assetIdentifierToMove, TreeNode<AssetNode> newParentEntry)
	{
		TreeNode<AssetNode> fileToMove = GetNodeFromIdentifier(assetIdentifierToMove).GetValueOrDefault();

		if (fileToMove == null)
		{
			Log.EngineLogger.Error($"Couldn't find asset node for asset {assetIdentifierToMove}");
			return .Err;
		}

		String newPathName = scope .();
		Path.Combine(newPathName, newParentEntry->Path, fileToMove->Name);

		if (File.Exists(newPathName))
		{
			Log.EngineLogger.Error("Can't move file. Target already exists.");
			return .Err;
		}

		if (fileToMove->IsDirectory)
		{
			if (Directory.Move(fileToMove->Path, newPathName) case .Err(let error))
			{
				Log.EngineLogger.Error($"Couldn't move directory from {fileToMove->Path} to {newPathName} ({error}).");
				return .Err;
			}
		}
		else
		{
			if (File.Move(fileToMove->Path, newPathName) case .Err(let error))
			{
				Log.EngineLogger.Error($"Couldn't move file from {fileToMove->Path} to {newPathName} ({error}).");
				return .Err;
			}
		}

		if (fileToMove->AssetFile != null)
		{
			if (File.Exists(fileToMove->AssetFile.AssetConfigPath))
			{
				String newConfigPathName = scope .(newPathName);
				newConfigPathName.Append(AssetFile.ConfigFileExtension);

				if (File.Move(fileToMove->AssetFile.AssetConfigPath, newConfigPathName) case .Err(let error))
				{
					Log.EngineLogger.Error($"Couldn't move asset description file from {fileToMove->AssetFile.AssetConfigPath} to {newConfigPathName} ({error}). Sorry :(");
					return .Err;
				}
			}
		}

		return .Ok;
	}
	
	/// Sets path to the directory that contains the engine assets.
	public void SetResourcesDirectory(StringView fileName)
	{
		ResourcesDirectory = fileName;

		_resourcesDirectoryNode?.ForEach(scope (node) => {
			_pathToAssetNode.Remove(node->Path);
			_identifierToAssetNode.Remove(node->Identifier);

			let handle = node->AssetFile?.AssetConfig?.AssetHandle;
			if (handle != null)
				_handleToAssetNode.Remove(handle.Value);
		});
		_assetRootNode.RemoveChild(_resourcesDirectoryNode);
		DeleteTreeAndChildren!(_resourcesDirectoryNode);
		_resourcesDirectoryNode = null;

		if (!Directory.Exists(ResourcesDirectory))
		{
			Log.EngineLogger.Error($"Resources directory \"{ResourcesDirectory}\" doesn't exist.");
			return;
		}
		
		AssetNode assetNode = new AssetNode();
		assetNode.Path = new String(ResourcesDirectory);
		assetNode.Name = new String();
		assetNode.Identifier = new AssetIdentifier("###resources");
		assetNode.IsDirectory = true;
		Path.GetFileName(assetNode.Path, assetNode.Name);

		_resourcesDirectoryNode = _assetRootNode.AddChild(assetNode);
		_pathToAssetNode.Add(assetNode.Path, _resourcesDirectoryNode);
		_identifierToAssetNode.Add(assetNode.Identifier, _resourcesDirectoryNode);

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

		// TODO: Watch resources folder?
		//SetupResourcesFileSystemWatcher();

		UpdateFiles();
	}

	/// Sets path to the directory that contains the game assets.
	public void SetAssetsDirectory(StringView fileName)
	{
		if (AssetsDirectory == fileName)
			return;

		AssetsDirectory = fileName;
		
		_assetsDirectoryNode?.ForEach(scope (node) => {
			_pathToAssetNode.Remove(node->Path);
			_identifierToAssetNode.Remove(node->Identifier);

			let handle = node->AssetFile?.AssetConfig?.AssetHandle;
			if (handle != null)
				_handleToAssetNode.Remove(handle.Value);
		});
		_assetRootNode.RemoveChild(_assetsDirectoryNode);
		DeleteTreeAndChildren!(_assetsDirectoryNode);
		_assetsDirectoryNode = null;
		
		if (!Directory.Exists(AssetsDirectory))
		{
			// TODO: This is not an error, if no project is loaded
			Log.EngineLogger.Warning($"Assets directory \"{AssetsDirectory}\" doesn't exist.");
			return;
		}
		
		AssetNode assetNode = new AssetNode();
		assetNode.Path = new String(AssetsDirectory);
		assetNode.Name = new String();
		assetNode.Identifier = new AssetIdentifier("###assets");
		assetNode.IsDirectory = true;
		Path.GetFileName(assetNode.Path, assetNode.Name);

		_assetsDirectoryNode = _assetRootNode.AddChild(assetNode);
		_pathToAssetNode.Add(assetNode.Path, _assetsDirectoryNode);
		_identifierToAssetNode.Add(assetNode.Identifier, _assetsDirectoryNode);

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

		SetupFileSystemWatcher();

		UpdateFiles();
	}

	private bool _fileTreeUpdateRequested = false;

	private float _fileSystemDebounce;

	/// Invokes the the file tree update on the mainthread.
	private void DeferFileTreeUpdate()
	{
		_fileSystemDebounce = FileSystemDebounceSeconds;

		if (!_fileTreeUpdateRequested)
		{
			_fileTreeUpdateRequested = true;

			Application.Instance.InvokeOnMainThread(new () =>
				{
					_fileSystemDebounce -= Application.Instance.GameTime.DeltaTime;
					
					if (_fileSystemDebounce > 0)
						return false;

					UpdateFiles();
					_fileTreeUpdateRequested = false;

					return true;
				});
		}
	}
	/// Initializes the FSW for the current ContentDirectory and registers the events.
	private void SetupFileSystemWatcher()
	{
		delete fsw;
		fsw = new FileSystemWatcher(_assetsDirectory);
		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}\")");

			FileContentChanged(filename);
		});

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

			DeferFileTreeUpdate();
		});

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

			DeferFileTreeUpdate();
		});

		fsw.OnRenamed.Add(new (oldName, newName) => {
			Log.EngineLogger.Trace($"File renamed (From \"{oldName}\" to \"{newName}\")");
			
			FileRenamed(oldName, newName);
			
			DeferFileTreeUpdate();
		});

		fsw.StartRaisingEvents();
	}

	/// Gets the tree node for the given filePath or .Err, 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;
	}

	/// Gets the tree node for the given asset identifier or .Err, 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>> GetNodeFromIdentifier(StringView identifier)
	{
		if (_identifierToAssetNode.TryGetValue(identifier, let treeNode))
		{
			return treeNode;
		}

		return .Err;
	}

	/// Gets the tree node for the given asset handle; or .Err, if no file exists with the given handle.
	/// @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>> GetNodeFromAssetHandle(AssetHandle assetHandle)
	{
		if (_handleToAssetNode.TryGetValue(assetHandle, let treeNode))
		{
			return treeNode;
		}

		return .Err;
	}

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

	/// Determines and set the Identifier for the given asset node.
	private void DetermineIdentifier(TreeNode<AssetNode> assetNode)
	{
		String identifier = scope .();

		// Get the Identifier which is simply the path relative to the asste root (either Resources- or Assets-Folder)
		if (assetNode.IsInSubtree(_resourcesDirectoryNode))
		{
			Path.GetRelativePath(assetNode->Path, _resourcesDirectoryNode->Path, identifier);
			identifier.Insert(0, AssetIdentifier.ResourcesPrefix);
		}
		else if (assetNode.IsInSubtree(_assetsDirectoryNode))
		{
			Path.GetRelativePath(assetNode->Path, _assetsDirectoryNode->Path, identifier);
			identifier.Insert(0, AssetIdentifier.AssetsPrefix);
		}

		delete assetNode->Identifier;
		assetNode->Identifier = new AssetIdentifier(identifier);
	}

	/// Scans the tree for orphaned nodes and removes them.
	void RemoveOrphanedEntries(TreeNode<AssetNode> node)
	{
		for (TreeNode<AssetNode> child in node.Children)
		{
			if (!Directory.Exists(child->Path) && !File.Exists(child->Path))
			{
				Log.EngineLogger.Trace($"Removing orphaned node for: \"{child->Path}\"");

				@child.Remove();

				// Remove entire subtree from all maps
				child.ForEach(scope (node) => {
					_pathToAssetNode.Remove(node->Path);
					_identifierToAssetNode.Remove(node->Identifier);

					if (node->AssetFile != null)
					{
						_handleToAssetNode.Remove(node->AssetFile.AssetConfig.AssetHandle);
					}

					Log.EngineLogger.Trace($"Removed orphaned node from tree {node->Path} ({node->AssetFile?.AssetConfig.AssetHandle})");
				});

				DeleteTreeAndChildren!(child);
			}
			else
			{
				RemoveOrphanedEntries(child);
			}
		}
	}

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

		// Cleanup entire tree first to avoid 
		RemoveOrphanedEntries(_assetRootNode);

		/// Creates a TreeNode for the given file path.
		/// @param fileName The name of the file/directory to add to the tree.
		/// @param isDirectory If True, the Entry will be added as a directory
		/// @param parentNode The node that is the parentNode in the tree
		TreeNode<AssetNode> CreateTreeNode(String filePath, bool isDirectory, TreeNode<AssetNode> parentNode)
		{
			AssetNode assetNode = new AssetNode();
			assetNode.Path = new String(filePath);
			assetNode.Name = new String();
			assetNode.IsDirectory = isDirectory;

			Path.GetFileName(filePath, assetNode.Name);

			TreeNode<AssetNode> treeNode = parentNode.AddChild(assetNode);

			DetermineIdentifier(treeNode);

			_pathToAssetNode.Add(assetNode.Path, treeNode);
			_identifierToAssetNode.Add(assetNode.Identifier, treeNode);

			// Only files get AssetFile and AssetHandle
			if (!isDirectory)
			{
				// TODO: Subassets -> Happens in processor!
				//GrabSubAssets(node);

				// TODO: Apparently directories were supposed to get an AssetFile? Makes sense, we wanted to have settings for directories, too!
				treeNode->AssetFile = AssetFile.LoadOrCreateAssetFile(_contentManager, assetNode);

				_handleToAssetNode.Add(assetNode.AssetFile.AssetConfig.AssetHandle, treeNode);
			}

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

			return treeNode;
		}

		/// If necessary adds a tree node for the given file path. For Directories also adds files and subdirectories.
		/// @param fileName The name of the file/directory to add to the tree.
		/// @param isDirectory If True, the Entry will be added as a directory
		/// @param parentNode The node that is the parentNode in the tree
		void AddEntryToTree(String filePath, bool isDirectory, TreeNode<AssetNode> parentNode)
		{
			// Try to find the node for the specified path in the given parent
			TreeNode<AssetNode> treeNode = parentNode.Children.Where(scope (node) => node.Value.Path == filePath).FirstOrDefault();

			// If no node was found -> create one
			if (treeNode == null)
				treeNode = CreateTreeNode(filePath, isDirectory, parentNode);

			if (isDirectory)
				AddSubdirectoriesAndFilesToTree(treeNode);
		}

		/// Adds the subdirectories of the given directory to the tree
		/// @param directoryNode The node of the directory whose subdirectories and files will be added.
		void AddSubdirectoriesAndFilesToTree(TreeNode<AssetNode> directoryNode)
		{
			// Filter that finds all entries of a directory.
			String filter = scope $"{directoryNode->Path}/*";

			String filePathBuffer = scope String(256);
			String fileExtensionBuffer = scope String(16);

			for (var entry in Directory.Enumerate(filter, .Directories | .Files))
			{
				entry.GetFilePath(filePathBuffer..Clear());
				filePathBuffer.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

				if (!entry.IsDirectory)
				{
					// For files we have to check whether or not the file ends with .ass

					fileExtensionBuffer.Clear();
					Path.GetExtension(filePathBuffer, fileExtensionBuffer);
					
					// Ignore meta files.
					if (fileExtensionBuffer.Equals(AssetFile.ConfigFileExtension, .OrdinalIgnoreCase))
						continue;
				}
				
				AddEntryToTree(filePathBuffer, entry.IsDirectory, directoryNode);
			}
		}

		if (!AssetsDirectory.IsWhiteSpace)
			AddSubdirectoriesAndFilesToTree(_assetsDirectoryNode);
		
		if (!ResourcesDirectory.IsWhiteSpace)
			AddSubdirectoriesAndFilesToTree(_resourcesDirectoryNode);
	}

	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.Combine(fileNameWithContentRoot, _assetsDirectory, 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;

		if (!(node->AssetFile?.UseNewAssetPipeline ?? false))
			OnFileContentChanged(node.Value);

		_fileSystemDebounce = FileSystemDebounceSeconds;

		Application.Instance.InvokeOnMainThread(new () => {
			_fileSystemDebounce -= Application.Instance.GameTime.DeltaTime;
			if (_fileSystemDebounce > 0)
				return false;

			node->AssetFile?.CheckForReprocessing();

			return true;
		});

	}

	private void FileRenamed(StringView oldFilePath, StringView newFilePath)
	{
		var oldFilePath;

		// Ignore Config files.
		if (oldFilePath.EndsWith(AssetFile.ConfigFileExtension))
			return;

		String oldFileNameWithContentRoot = scope .();
		Path.InternalCombine(oldFileNameWithContentRoot, _assetsDirectory, oldFilePath);
		
		String newFileNameWithContentRoot = scope .();
		Path.InternalCombine(newFileNameWithContentRoot, _assetsDirectory, newFilePath);
		
		// Rename config file
		{
			String oldConfigFileName = scope $"{oldFileNameWithContentRoot}{AssetFile.ConfigFileExtension}";
			String newConfigFileName = scope $"{newFileNameWithContentRoot}{AssetFile.ConfigFileExtension}";

			if (File.Exists(oldConfigFileName) && !File.Exists(newConfigFileName))
			{
				// TODO: Actually Move, but it's a bit complicated to manage cases, where external programs move the original file and
				// rename an new file to the original name. So just copy the config to retain it.
				// I don't think there is a clean way to solve this.
				if (File.Copy(oldConfigFileName, newConfigFileName) case .Err(let value))
				{
					Log.EngineLogger.Error($"Failed to move file {oldConfigFileName} to {newConfigFileName}. Code: {value}");
				}
			}
		}

		var nodeResult = GetNodeFromPath(oldFileNameWithContentRoot);

		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 \"{oldFileNameWithContentRoot}\"");
			return;
		}

		// Save the old identifier
		String oldIdentifier = scope .(node->Identifier);

		_pathToAssetNode.Remove(oldFileNameWithContentRoot);
		_identifierToAssetNode.Remove(node->Identifier);

		node->Path.Set(newFileNameWithContentRoot);
		_pathToAssetNode.Add(node->Path, node);
		
		// Updates node->Identifier
		DetermineIdentifier(node);

		_identifierToAssetNode.Add(node->Identifier, node);
		
		node->Name.Clear();
		Path.GetFileName(newFileNameWithContentRoot, node->Name);

		// Directories have no AssetFile?
		if (node->AssetFile != null)
		{
			node->Path.Set(newFilePath);

			delete node->Identifier;
			node->Identifier = new AssetIdentifier(newFilePath);

			node->AssetFile.[Friend]_assetConfigPath.Set(node->Path);
			node->AssetFile.[Friend]_assetConfigPath.Append(AssetFile.ConfigFileExtension);
		}

		// Fire renamed event (e.g. because EditorContentManager needs to know)
		OnFileRenamed(node.Value, oldIdentifier);
	}

	public delegate void FileContentChangedFunc(AssetNode node);

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

	public delegate void FileRenamedFunc(AssetNode node, StringView oldName);

	public Event<FileRenamedFunc> OnFileRenamed ~ _.Dispose();

	public enum CopyError
	{
		TargetDirectoryIsNotDirectory,
		SourceDoesntExist,
		TargetExists
	}

	public enum OverwriteMode
	{
		None,
		Overwrite,
		KeepBoth,
		Combine
 	}

	/*public Result<BackgroundTask> CopyExternFileToNodeBackground(TreeNode<AssetNode> targetDirectory, String sourcePath, OverwriteMode overwrite)
	{
		if (!targetDirectory->IsDirectory)
			return .Err;
		
		FileInfo sourceInfo = scope FileInfo(sourcePath);

		if (!sourceInfo.Exists)
			return .Err;

		CopyBackgroundTask copyTask = new CopyBackgroundTask(sourcePath, targetDirectory->Path);

		EditorApp.Instance.BackgroundTaskManager.StartBackgroundTask(copyTask);

		return copyTask;
	}*/

	public Result<BackgroundTask> CopyExternFilesToNodeBackground(TreeNode<AssetNode> targetDirectory, List<String> sourcePaths, OverwriteMode overwrite)
	{
		if (!targetDirectory->IsDirectory)
			return .Err;
		
		/*FileInfo sourceInfo = scope FileInfo(sourcePath);

		if (!sourceInfo.Exists)
			return .Err;*/

		CopyBackgroundTask copyTask = new CopyBackgroundTask(sourcePaths, targetDirectory->Path);
		copyTask.DeleteWhenEnded = true;

		EditorApp.Instance.BackgroundTaskManager.StartBackgroundTask(copyTask);

		return copyTask;
	}

	public Result<void, CopyError> CopyExternFileToNode(TreeNode<AssetNode> targetDirectory, String sourcePath, OverwriteMode overwrite)
	{
		if (!targetDirectory->IsDirectory)
			return .Err(.TargetDirectoryIsNotDirectory);

		if (!File.Exists(sourcePath))
			return .Err(.SourceDoesntExist);


		FileInfo sourceInfo = scope FileInfo(sourcePath);

		String fileName = scope .();
		Path.GetFileName(sourcePath, fileName);

		String targetPath = scope .();
		Path.Combine(targetPath, targetDirectory->Path, fileName);
		
		FileInfo targetInfo = scope FileInfo(targetPath);
		
		if (sourceInfo.IsDirectory)
		{
			bool forceOverwrite = false;
			if (targetInfo.Exists)
			{
				switch (overwrite)
				{
				case .KeepBoth:
					String fileExtension = scope .();
					Path.GetExtension(targetPath, fileExtension);
					Path.FindFreePath(targetDirectory->Path, fileName.Substring(0..<^fileExtension.Length), fileExtension, targetPath..Clear());
				case .Overwrite:
					forceOverwrite = true;
				default:
					return .Err(.TargetExists);
				}
			}
		}
		else
		{
			bool forceOverwrite = false;
			if (targetInfo.Exists)
			{
				switch (overwrite)
				{
				case .KeepBoth:
					String fileExtension = scope .();
					Path.GetExtension(targetPath, fileExtension);
					Path.FindFreePath(targetDirectory->Path, fileName.Substring(0..<^fileExtension.Length), fileExtension, targetPath..Clear());
				case .Overwrite:
					forceOverwrite = true;
				default:
					return .Err(.TargetExists);
				}
			}

			File.Copy(sourcePath, targetPath, forceOverwrite);
		}

		return .Ok;
	}
}
