using System;
using System.IO;
using System.Collections;
using System.Diagnostics;
using ImGui;
using System.Threading;

namespace GlitchyEditor.Multithreading;

class CopyBackgroundTask : BackgroundTask
{
	private List<String> _sourcePath ~ {ClearAndDeleteItems!(_); delete:append _;};
	private String _targetDirectoryPath ~ delete:append _;

	private int _totalEntriesToCopy;
	private append Queue<CopyInfo> _pathsToCopy = .() ~ ClearAndDeleteItems!(_);
	private append Queue<CopyInfo> _scanQueue = .() ~ ClearAndDeleteItems!(_);
	/// Contains the file paths, that will definitely exist after copying. This is used for file renaming during scanning.
	private append HashSet<StringView> _targetPaths = .();
	
	private enum ConflictResolutionMode
	{
		None,
		OverwriteFile = 1,
		KeepBoth = 2,
		CombineDirectories = 4,
		SkipFiles = 8,
		SkipDirectories = 16,
		SkipNotFound = 32,
		IgnoreUnexpectedErrors = 64
	}

	private ConflictResolutionMode _nextFileMode;
	private ConflictResolutionMode _allFilesMode;

	private ConflictResolutionMode CurrentFileMode => _nextFileMode | _allFilesMode;

	private class CopyInfo
	{
		public append String SourcePath = .();
		// If this entry is actually inside a directory we copy, this contains the path of this entry inside this directory.
		public append String SubPath = .();
		public append String TargetPath = .();
		public ConflictResolutionMode ConflictResolutionMode;

		[AllowAppend]
		public this(StringView sourcePath, StringView targetPath, ConflictResolutionMode conflictResolutionMode)
		{
			SourcePath.Set(sourcePath);
			TargetPath.Set(targetPath);
			ConflictResolutionMode = conflictResolutionMode;
		}
	}

	public bool ScanningFiles => !_scanQueue.IsEmpty;

	[AllowAppend]
	public this(List<String> sourcePaths, StringView targetDirectoryPath)
	{
		List<String> sourcePathList = append List<String>(sourcePaths.Count);
		String targetDirectoryPathStr = append String(targetDirectoryPath);

		_sourcePath = sourcePathList;
		_targetDirectoryPath = targetDirectoryPathStr;

		for (String path in sourcePaths)
		{
			_sourcePath.Add(new String(path));
			_scanQueue.Add(new CopyInfo(path, "", .None));
		}

		_totalEntriesToCopy = sourcePaths.Count;
	}
	
	public enum CopyError : IDisposable
	{
		case None;
		case TargetDirectoryIsNotDirectory;
		case TargetFileExists(String FileName);
		case TargetDirectoryExists(String DirectoryName);
		case EntryNotFound(String SourcePath);
		case UnexpectedError(String Message);

		public void Dispose()
		{
			switch (this)
			{
			case .None:
			case .TargetDirectoryIsNotDirectory:
			case .TargetFileExists(let FileName):
				delete FileName;
			case .TargetDirectoryExists(let DirectoryName):
				delete DirectoryName;
			case .EntryNotFound(let SourcePath):
				delete SourcePath;
			case .UnexpectedError(let Message):
				delete Message;
			}
		}
	}

	private Result<void, CopyError> CollectFilesToCopy()
	{
		CopyInfo currentEntry = null;
		defer
		{
			if (currentEntry != null && @return case .Err)
			{
				_scanQueue.AddFront(currentEntry);
			}
		}

		void AddToCopyQueue(CopyInfo entry, StringView targetPath, ConflictResolutionMode conflictResolutionMode)
		{
			entry.TargetPath.Set(targetPath);
			entry.ConflictResolutionMode = conflictResolutionMode;
			_pathsToCopy.Add(entry);
			_targetPaths.Add(entry.TargetPath);
		}

		while (!_scanQueue.IsEmpty && Running)
		{
			currentEntry = _scanQueue.PopFront();
			
			let sourcePath = (StringView)currentEntry.SourcePath;

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

			let targetPath = scope String();
			Path.Combine(targetPath, _targetDirectoryPath, currentEntry.SubPath, fileName);

			if (Directory.Exists(sourcePath))
			{
				bool skip = false;

				if (Directory.Exists(targetPath) || _targetPaths.Contains(targetPath))
				{
					if (CurrentFileMode.HasFlag(.KeepBoth) || targetPath == sourcePath)
					{
						Path.FindFreePath(_targetDirectoryPath, fileName, "", targetPath, fileName, _targetPaths);
					}
					else if (CurrentFileMode.HasFlag(.SkipDirectories))
					{
						skip = true;
					}
					else if (Enum.HasAnyFlag(CurrentFileMode, .CombineDirectories))
					{
						// Intentionally do nothing.
					}
					else
					{
						return .Err(.TargetDirectoryExists(new String(fileName)));
					}
				}
				
				if (skip)
				{
					delete currentEntry;
				}
				else
				{
					// Current node is ready, add it to copy-queue.
					AddToCopyQueue(currentEntry, targetPath, CurrentFileMode);
					
					// Add nested files and directories to the scan-queue.
					for (FileFindEntry e in Directory.Enumerate(currentEntry.SourcePath))
					{
						let entryPath = scope String();
						e.GetFilePath(entryPath);

						let newEntry = new CopyInfo(entryPath, targetPath, .None);
						_scanQueue.Add(newEntry);

						Path.Combine(newEntry.SubPath, currentEntry.SubPath, fileName);
					}
				}

				_nextFileMode = .None;
			}
			else if (File.Exists(sourcePath))
			{
				bool skip = false;

				if (File.Exists(targetPath) || _targetPaths.Contains(targetPath))
				{
					if (CurrentFileMode.HasFlag(.KeepBoth) || targetPath == sourcePath)
					{
						let fileExtension = scope String();
						Path.GetExtension(targetPath, fileExtension);

						StringView fileNameWithoutExtension = fileName.Substring(0..<^fileExtension.Length);
	
						Path.FindFreePath(_targetDirectoryPath, fileNameWithoutExtension, fileExtension, targetPath, fileName, _targetPaths);
					}
					else if (CurrentFileMode.HasFlag(.SkipFiles))
					{
						skip = true;
					}
					else if (Enum.HasAnyFlag(CurrentFileMode, .OverwriteFile))
					{
						// Intentionally do nothing.
					}
					else
					{
						return .Err(.TargetFileExists(new String(fileName)));
					}
				}
				
				if (skip)
				{
					delete currentEntry;
				}
				else
				{
					AddToCopyQueue(currentEntry, targetPath, CurrentFileMode);
				}

				_nextFileMode = .None;
			}
		}

		_totalEntriesToCopy = _pathsToCopy.Count;

		return .Ok;
	}

	private CopyError _currentError = .None ~ _.Dispose();

	public override RunResult Run()
	{
		_currentError.Dispose();
		_currentError = .None;
		if (CollectFilesToCopy() case .Err(out _currentError) || Paused)
		{
			return .Pause;
		}
		
		while (!_pathsToCopy.IsEmpty && Running)
		{
			CopyInfo currentPath = _pathsToCopy.Peek();

			if (CopyPath(currentPath) case .Err(out _currentError) || Paused)
			{
				return .Pause;
			}

			_pathsToCopy.PopFront();
			delete currentPath;
		}

		return .Finished;
	}

	private Result<void, CopyError> CopyPath(CopyInfo copyInfo)
	{
		if (Directory.Exists(copyInfo.SourcePath))
		{
			if (Directory.Exists(copyInfo.TargetPath))
			{
				if (copyInfo.ConflictResolutionMode.HasFlag(.CombineDirectories))
				{
					// We don't have to do anything, because we will be using the target directory.
					return .Ok;
				}
				else if (!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
				{
					return .Err(.UnexpectedError(new $"The target directory already exists, but it wasn't properly handled during the scanning phase."));
				}
			}

			if (Directory.CreateDirectory(copyInfo.TargetPath) case .Err(let err) &&
				!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
			{
				return .Err(.UnexpectedError(new $"Failed to create Directory {copyInfo.TargetPath}: {err}"));
			}
		}
		else if (File.Exists(copyInfo.SourcePath))
		{
			bool forceOverwrite = false;
			if (File.Exists(copyInfo.TargetPath))
			{
				if (copyInfo.ConflictResolutionMode.HasFlag(.OverwriteFile))
				{
					forceOverwrite = true;
				}
				else if (!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
				{
					return .Err(.UnexpectedError(new $"The target file already exists, but it wasn't properly handled during the scanning phase."));
				}
			}
			
			if (File.Copy(copyInfo.SourcePath, copyInfo.TargetPath, forceOverwrite) case .Err(let err) &&
				!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
			{
				return .Err(.UnexpectedError(new $"Failed to copy File {copyInfo.TargetPath}: {err}"));
			}
		}
		else
		{
			if (copyInfo.ConflictResolutionMode.HasFlag(.SkipNotFound))
				return .Ok;
			else
				return .Err(.EntryNotFound(new String(copyInfo.SourcePath)));
		}

		return .Ok;
	}

	public override void OnRenderPopup()
	{
		String title = scope .("Copying files...");

		if (ImGui.Begin(title, null, .NoDocking | .NoCollapse | .Modal | .NoResize | .AlwaysAutoResize))
		{
			if (ScanningFiles)
			{
				ImGui.ProgressBar(-1.0f * (float)ImGui.GetTime(), .(-1, 0), scope $"Found {_pathsToCopy.Count} files...");
			}
			else
			{
				int copiedFiles = _totalEntriesToCopy - _pathsToCopy.Count;

				ImGui.ProgressBar(copiedFiles / _totalEntriesToCopy, .(-1, 0), scope $"Copied {copiedFiles} / {_totalEntriesToCopy} files.");
			}

			
			switch (_currentError)
			{
			case .TargetFileExists(let fileName):
				ImGui.PushStyleColor(.Text, 0xFF0000FF);
				ImGui.TextUnformatted(scope $"A file with the name \"{fileName}\" already exists.");
				ImGui.PopStyleColor();
			case .TargetDirectoryExists(let directoryName):
				ImGui.PushStyleColor(.Text, 0xFF0000FF);
				ImGui.TextUnformatted(scope $"A directory with the name \"{directoryName}\" already exists.");
				ImGui.PopStyleColor();
			case .EntryNotFound(let SourcePath):
				ImGui.PushStyleColor(.Text, 0xFF0000FF);
				ImGui.TextUnformatted(scope $"\"{SourcePath}\" doesn't exist.");
				ImGui.PopStyleColor();
			case .UnexpectedError(let Message):
				ImGui.PushStyleColor(.Text, 0xFF0000FF);
				ImGui.TextUnformatted(scope $"Unexpected error: {Message}");
				ImGui.PopStyleColor();
			default:
				// Do nothing
			}
			
			if (ImGui.BeginTable("buttonTable", 2))
			{
				switch (_currentError)
				{
				case .None:
					// Do nothing
				case .TargetDirectoryIsNotDirectory:
					// This shouldn't be possible
					Debug.Break();
				case .TargetFileExists(let fileName):
					ImGui.TableNextRow();
					ImGui.TableSetColumnIndex(0);

					if (ImGui.Button("Overwrite the target file."))
					{
						_nextFileMode = .OverwriteFile;
						Continue();
					}
					ImGui.TableSetColumnIndex(1);
					if (ImGui.Button("Overwrite all conflicting target files."))
					{
						_allFilesMode |= .OverwriteFile;
						Continue();
					}
					
					ImGui.TableNextRow();
					ImGui.TableSetColumnIndex(0);

					if (ImGui.Button("Rename copied file."))
					{
						_nextFileMode = .KeepBoth;
						Continue();
					}
					ImGui.TableSetColumnIndex(1);
					if (ImGui.Button("Rename all conflicting copied files."))
					{
						_allFilesMode |= .KeepBoth;
						Continue();
					}
					
					ImGui.TableNextRow();
					ImGui.TableSetColumnIndex(0);

					if (ImGui.Button("Skip this file."))
					{
						_nextFileMode = .SkipFiles;
						Continue();
					}
					ImGui.TableSetColumnIndex(1);
					if (ImGui.Button("Skip all conflicting files."))
					{
						_allFilesMode |= .SkipFiles;
						Continue();
					}
					
				case .TargetDirectoryExists(let directoryName):
					ImGui.TableNextRow();
					ImGui.TableSetColumnIndex(0);
	
					if (ImGui.Button("Combine with target."))
					{
						_nextFileMode |= .CombineDirectories;
						Continue();
					}
					ImGui.TableSetColumnIndex(1);
					if (ImGui.Button("Combine all conflicting directories."))
					{
						_allFilesMode |= .CombineDirectories;
						Continue();
					}

					ImGui.TableNextRow();
					ImGui.TableSetColumnIndex(0);
					
					if (ImGui.Button("Rename copied directory."))
					{
						_nextFileMode |= .KeepBoth;
						Continue();
					}
					ImGui.TableSetColumnIndex(1);
					if (ImGui.Button("Rename all conflicting directories."))
					{
						_allFilesMode |= .KeepBoth;
						Continue();
					}
					
					ImGui.TableNextRow();
					ImGui.TableSetColumnIndex(0);

					if (ImGui.Button("Skip this directory."))
					{
						_nextFileMode |= .SkipDirectories;
						Continue();
					}
					ImGui.TableSetColumnIndex(1);
					if (ImGui.Button("Skip all conflicting directories."))
					{
						_allFilesMode |= .SkipFiles;
						Continue();
					}
				case .EntryNotFound(let SourcePath):
					if (ImGui.Button("Skip"))
					{
						_nextFileMode |= .SkipNotFound;
						Continue();
					}
					ImGui.SameLine();
					if (ImGui.Button("Skip all"))
					{
						_allFilesMode |= .SkipNotFound;
						Continue();
					}
				case .UnexpectedError(let Message):
					if (ImGui.Button("Retry"))
					{
						Continue();
					}
					ImGui.SameLine();
					if (ImGui.Button("Ignore"))
					{
						_nextFileMode |= .IgnoreUnexpectedErrors;
						Continue();
					}
					ImGui.SameLine();
					if (ImGui.Button("Ignore all unexpected"))
					{
						_allFilesMode |= .IgnoreUnexpectedErrors;
						Continue();
					}
				}
				
				ImGui.TableNextRow();
				ImGui.TableNextColumn();

				if (_currentError case .None)
				{
					if (Paused)
					{
						if (ImGui.Button("Continue"))
						{
							Continue();
						}
					}
					else
					{
						if (ImGui.Button("Pause"))
						{
							Pause();
						}
					}
	
					ImGui.SameLine();
				}
	
				if (ImGui.Button("Cancel"))
				{
					Abort();
				}
				ImGui.EndTable();
			}
			
			ImGui.End();
		}
	}
}
