using System;
using System.IO;
using Bon;
using GlitchyEngine;
using GlitchyEngine.Scripting;
using Xml_Beef;

namespace GlitchyEditor;

[BonTarget]
class Project
{
	[BonInclude]
	private String _projectName ~ delete _;

	[BonIgnore]
	private String _workspacePath ~ delete _;
	
	[BonIgnore]
	private String _assetsFolder ~ delete _;

	[BonIgnore]
	private String _scriptFolder ~ delete _;
	
	[BonIgnore]
	private ProjectUserSettings _userSettings ~ delete _;

	public StringView Name => _projectName;
	public StringView WorkspacePath => _workspacePath;

	public StringView AssetsFolder => _assetsFolder;

	public ProjectUserSettings UserSettings => _userSettings;

	[AllowAppend]
	private this(StringView workspacePath)
	{
		_workspacePath = new String(workspacePath);

		_assetsFolder = new String();
		PathInProject(_assetsFolder, "Assets");

		InitUserSettings();
	}
	
	/// Writes the the absolute path of the give project-file into target.
	/// @param target The string that will contain the full path.
	/// @param relativePath The path of the file relative to the workspace folder.
	public void PathInProject(String target, StringView relativePath)
	{
		Path.Combine(target, WorkspacePath, relativePath);
	}

	/// Creates a new scoped string that contains the absolute path of the give project-file
	/// @param relativePath The path of the file relative to the workspace folder.
	public mixin GetScopedPath(StringView relativePath)
	{
		String target = scope:mixin String();

		PathInProject(target, relativePath);

		target
	}

	public void GetPathToScriptSolutionFile(String outTarget)
	{
		PathInProject(outTarget, scope $"{Name}.slnx");
	}
	
	public void GetPathToScriptProjectFile(String outTarget)
	{
		PathInProject(outTarget, scope $"{Name}.csproj");
	}
	
	private static void GetScriptCoreProjectFilePath(String outTarget)
	{
		Directory.GetCurrentDirectory(outTarget);
		Path.Combine(outTarget, "../ScriptCore/ScriptCore.csproj");
		Path.ToActualPath(outTarget);
	}

	/// Loads or creates the user specific settings for this project.
	private void InitUserSettings()
	{
		_userSettings = new ProjectUserSettings();

		String userSettingsFile = scope .();
		PathInProject(userSettingsFile, ProjectUserSettings.FileName);

		// It's not a problem if we couldn't find or deserialize the settings. We will simply restore the defaults.
		if (Bon.DeserializeFromFile(ref _userSettings, userSettingsFile) case .Err)
		{
			// Log anyway, just in case...
			Log.EngineLogger.Info($"Failed to deserialize project user settings file \"{userSettingsFile}\".");
			_userSettings.RestoreDefaults();
		}
	}

	public void SaveUserSettings()
	{
		if (_userSettings == null || !_userSettings.SettignsDirty)
			return;

		String userSettingsFile = scope .();
		PathInProject(userSettingsFile, ProjectUserSettings.FileName);

		// It's not a big problem, if we fail to serialize this file.
		if (Bon.SerializeIntoFile(_userSettings, userSettingsFile) case .Err)
		{
			// Log anyway, just in case...
			Log.EngineLogger.Error($"Failed to deserialize project user settings file \"{userSettingsFile}\".");
		}
	}

	/// Creates a new project with the given directory and name.
	public static Result<Project> CreateNew(StringView projectDirectory, StringView projectName)
	{
		Project project = new Project(projectDirectory);
		project._projectName = new String(projectName);

		String settingsFile = scope .();
		project.PathInProject(settingsFile, "project.gep");

		/// This is not really fatal, however the project might end up unusable
		if (Bon.SerializeIntoFile(project, settingsFile) case .Err)
		{
			Log.EngineLogger.Warning($"Failed to save project file \"{settingsFile}\".");
			delete project;

			return .Err;
		}
	
		return project;
	}

	/// Creates a new project with the given name in the specified directory basend on the provided template.
	public static Result<Project> CreateNewFromTemplate(StringView projectDirectory, StringView projectName, StringView templatePath)
	{
		Project newProject = Try!(Project.CreateNew(projectDirectory, projectName));

		if (Project.InitializeTemplate(newProject, templatePath) case .Err)
		{
			delete newProject;
			return .Err;
		}

		return newProject;
	}

	/// Initializes a project with the provided template.
	public static Result<void> InitializeTemplate(Project project, StringView templatePath)
	{
		Try!(CopyTemplate(project, templatePath));
		
		Try!(RenameAndFixupFiles(project.WorkspacePath, project));

		Try!(project.FixupScriptCorePath());

		return .Ok;
	}

	/// Copies the template files into the given project.
	private static Result<void> CopyTemplate(Project project, StringView templatePath)
	{
		if (Directory.Copy(templatePath, project.WorkspacePath) case .Err)
		{
			Log.EngineLogger.Error($"CopyTemplate: Failed to copy template {templatePath} to {project.WorkspacePath}.");
			return .Err;
		}

		return .Ok;
	}

	const String ProjectNamePlaceholder = "[ProjectName]";
	
	/// Moves files so that [ProjectName] in their paths will be replaced by the actual project name
	private static Result<void> RenameAndFixupFiles(StringView directoryPath, Project project)
	{
		String fullPath = scope .(256);
		String modifiedFileName = scope String(256);

		for (FileFindEntry entry in Directory.Enumerate(scope $"{directoryPath}/*", .Files | .Directories))
		{
			entry.GetFilePath(fullPath..Clear());
			
			if (fullPath.Contains(ProjectNamePlaceholder))
			{
				modifiedFileName.Set(fullPath);
				modifiedFileName.Replace(ProjectNamePlaceholder, project.Name);

				if (entry.IsDirectory)
					Try!(Directory.Move(fullPath, modifiedFileName));
				else
					Try!(File.Move(fullPath, modifiedFileName));

				Swap!(fullPath, modifiedFileName);
			}

			if (entry.IsDirectory)
			{
				Try!(RenameAndFixupFiles(fullPath, project));
			}
			else
			{
				Try!(FixupFile(fullPath, project));
			}
		}

		return .Ok;
	}

	// Fixups a file by replacing placeholders for e.g. the project name.
	private static Result<void> FixupFile(StringView filePath, Project project)
	{
		String fileContent = scope String();
		if (File.ReadAllText(filePath, fileContent, true) case .Err)
		{
			Log.EngineLogger.Error($"FixupFile: Failed to read file {filePath}.");
			return .Err;
		}

		if (!fileContent.Contains(ProjectNamePlaceholder))
			return .Ok;

		fileContent.Replace(ProjectNamePlaceholder, project.Name);

		if (File.WriteAllText(filePath, fileContent, false) case .Err)
		{
			Log.EngineLogger.Error($"FixupFile: Failed to write file {filePath}.");
			return .Err;
		}

		return .Ok;
	}

	public static Result<Project> Load(StringView projectDirectory)
	{
		Project project = new Project(projectDirectory);

		String settingsFile = scope .();
		project.PathInProject(settingsFile, "project.gep");

		if (File.Exists(settingsFile))
		{
			Result<void> result = Bon.DeserializeFromFile(ref project, settingsFile);

			if (result case .Err)
				Log.EngineLogger.Warning($"Failed to deserialize project file \"{settingsFile}\".");
		}
		else
		{
			Log.EngineLogger.Warning($"Project file \"{settingsFile}\" doesn't exist.");

			if (project.Name.IsWhiteSpace)
			{
				project._projectName ??= new String();

				Path.GetFileName(project.WorkspacePath, project._projectName);
				
				/// This is not really fatal, however the project might end up unusable
				if (Bon.SerializeIntoFile(project, settingsFile) case .Err)
				{
					Log.EngineLogger.Warning($"Failed to save project file \"{settingsFile}\".");
					delete project;

					return null;
				}
			}
		}

		Try!(project.FixupScriptCorePath());
		
		return project;
	}
	
	/// If necessary adds or removes the reference to ScriptCore.csproj from the solution (.slnx) file.
	private Result<void> FixupScriptSolutionFile(bool referenceScriptCoreProject)
	{
		String solutionPath = scope .();
		GetPathToScriptSolutionFile(solutionPath);

		String solutionFileContent = scope String();
		if (File.ReadAllText(solutionPath, solutionFileContent, true) case .Err)
		{
			Log.EngineLogger.Error($"FixupFile: Failed to read solution file {solutionPath}.");
			return .Err;
		}

		Xml solution = new Xml();
		solution.PreserveWhitespace = true;
		defer delete solution;

		solution.LoadFromString(solutionFileContent);

		XmlNode scriptCoreProjectReference = null;
		for (XmlNode projectNode in solution.DocumentElement.EnumerateNodes("Project", recursive: true))
		{
			if (projectNode.TryGetAttribute("Path", let includeAttribute))
			{
				if (includeAttribute.Value.Contains("ScriptCore.csproj", true))
				{
					scriptCoreProjectReference = projectNode;
					break;
				}
			}
		}

		if (referenceScriptCoreProject)
		{
			Log.EngineLogger.Info("Debug mode enabled. Referencing ScriptCore.csproj in Script-Solution.");

			if (scriptCoreProjectReference == null)
			{
				Log.EngineLogger.Warning("No reference to ScriptCore.csproj found. Adding...");
				scriptCoreProjectReference = solution.DocumentElement.AddChild("Project");
			}

			String projectFilePath = scope .();
			GetScriptCoreProjectFilePath(projectFilePath);
			scriptCoreProjectReference["Path"] = projectFilePath;
		}
		else if (scriptCoreProjectReference != null)
		{
			Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing...");
			scriptCoreProjectReference.RemoveFromParent(true);
		}
		
		String modifiedSolution = new String(solutionFileContent.Length * 2);
		defer delete modifiedSolution;
		solution.SaveToString(modifiedSolution);

		if (File.WriteAllText(solutionPath, modifiedSolution) case .Err)
		{
			Log.EngineLogger.Error($"Failed to modify {solutionPath}");
			return .Err;
		}

		return .Ok;
	}
	
	/// If necessary adds or removes the reference to ScriptCore.dll from the project (.csproj) file.
	private Result<void> FixupScriptProjectFile(bool referenceScriptCoreProject)
	{
		String csprojPath = scope .();
		GetPathToScriptProjectFile(csprojPath);

		String projectFileContent = scope String();
		if (File.ReadAllText(csprojPath, projectFileContent, true) case .Err)
		{
			Log.EngineLogger.Error($"FixupFile: Failed to read project file {csprojPath}.");
			return .Err;
		}

		Xml project = new Xml();
		project.PreserveWhitespace = true;
		defer delete project;

		project.LoadFromString(projectFileContent);
		
		// Try to find a reference to the csproj
		XmlNode scriptCoreProjectReference = null;
		for (XmlNode projectReferenceNode in project.DocumentElement.EnumerateNodes("ProjectReference", recursive: true))
		{
			if (projectReferenceNode.TryGetAttribute("Include", let includeAttribute))
			{
				if (includeAttribute.Value.Contains("ScriptCore.csproj", true))
				{
					scriptCoreProjectReference = projectReferenceNode;
					break;
				}
			}
		}

		// Try to find a reference to the dll
		XmlNode dllReferenceNode = project.DocumentElement.Find("Reference", "Include", "ScriptCore", recursive: true);

		if (referenceScriptCoreProject)
		{
			Log.EngineLogger.Info("Debug mode enabled. Referencing ScriptCore.csproj in Script-Project.");

			if (dllReferenceNode != null)
			{
				Log.EngineLogger.Warning("Reference to ScriptCore.dll found. Removing...");
				dllReferenceNode.RemoveFromParent(true);
			}

			if (scriptCoreProjectReference == null)
			{
				Log.EngineLogger.Warning("Reference to ScriptCore.csproj missing. Adding...");

				XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true);

				scriptCoreProjectReference = itemGroup.AddChild("ProjectReference");

				String scriptCoreProjectPath = scope .();
				Directory.GetCurrentDirectory(scriptCoreProjectPath);
				Path.Combine(scriptCoreProjectPath, "../ScriptCore/ScriptCore.csproj");
				Path.ToActualPath(scriptCoreProjectPath);

				scriptCoreProjectReference.SetAttribute("Include", scriptCoreProjectPath);
				scriptCoreProjectReference.SetAttribute("OutputItemType", "Analyzer");
				scriptCoreProjectReference.SetAttribute("ReferenceOutputAssembly", "true");
			}
		}
		else
		{
			if (scriptCoreProjectReference != null)
			{
				Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing...");
				scriptCoreProjectReference.RemoveFromParent(true);
			}

			if (dllReferenceNode == null)
			{
				Log.EngineLogger.Warning("Reference to ScriptCore.dll missing. Adding...");

				XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true);

				dllReferenceNode = itemGroup.AddChild("Reference");
			}

			dllReferenceNode.SetAttribute("Include", "ScriptCore");
			dllReferenceNode.SetAttribute("OutputItemType", "Analyzer");
			dllReferenceNode.SetAttribute("ReferenceOutputAssembly", "true");

			XmlNode hintPathNode = dllReferenceNode.Find("HintPath");
			if (hintPathNode == null)
			{
				hintPathNode = dllReferenceNode.AddChild("HintPath");
			}

			String pathToScriptCoreDll = scope .();
			Directory.GetCurrentDirectory(pathToScriptCoreDll);
			Path.Combine(pathToScriptCoreDll, ScriptEngine.ScriptCorePath);
			Path.ToActualPath(pathToScriptCoreDll);

			if (!File.Exists(pathToScriptCoreDll))
			{
				Log.EngineLogger.Error($"Could not fild \"{pathToScriptCoreDll}\"");
				return .Err;
			}	

			hintPathNode.SetText(pathToScriptCoreDll);
		}

		String modfiedProject = new String(projectFileContent.Length * 2);
		defer delete modfiedProject;
		project.SaveToString(modfiedProject);

		if (File.WriteAllText(csprojPath, modfiedProject) case .Err)
		{
			Log.EngineLogger.Error($"Failed to modify {csprojPath}");
		}

		return .Ok;
	}	

	/// Fixes the references to ScriptCore in the script-project and solution file.
	public Result<void> FixupScriptCorePath()
	{
		bool referenceScriptCoreProject = false;

#if DEBUG
		Settings settings = Application.Instance.Settings;
		referenceScriptCoreProject = settings.DevSettings.UseScriptCoreDll;
#endif

		Try!(FixupScriptSolutionFile(referenceScriptCoreProject));
		Try!(FixupScriptProjectFile(referenceScriptCoreProject));

		return .Ok;
	}
}