using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Xml;
using Tesla.Net;

namespace TeslaConsole;

/// <summary>Role a launch entry fills; only meaningful for products that prompt with the start-params dialog.</summary>
public enum AppHostType
{
	None,
	GameClient,
	LiveCamera,
	MissionReview
}

/// <summary>One launchable application a product registers on a pod.</summary>
public class LaunchEntryDefinition
{
	public Guid LaunchKey;

	public string DisplayName = "";

	public string Exe = "";

	/// <summary>Arguments; the token "{res}" expands to " -res W H" when a custom resolution is chosen, else "".</summary>
	public string Args = "";

	/// <summary>Optional; when null/empty the working directory is derived from <see cref="Exe"/>.</summary>
	public string WorkingDirectory;

	public bool AutoRestart = true;

	public AppHostType HostType;

	/// <summary>Builds the wire <see cref="LaunchData"/> for this entry, expanding {res} from the optional resolution.</summary>
	public LaunchData ToLaunchData(Size? resolution)
	{
		string resArg = resolution.HasValue
			? $" -res {resolution.Value.Width} {resolution.Value.Height}"
			: "";
		LaunchData data = default(LaunchData);
		data.LaunchPair.DisplayName = DisplayName;
		data.LaunchPair.LaunchKey = LaunchKey;
		data.ExeFile = Exe;
		data.Arguments = (Args ?? "").Replace("{res}", resArg);
		data.WorkingDirectory = string.IsNullOrEmpty(WorkingDirectory)
			? Path.GetDirectoryName(Exe)
			: WorkingDirectory;
		data.AutoRestart = AutoRestart;
		return data;
	}
}

/// <summary>An installable product: an install/package key plus the launch entries it registers.</summary>
public class ProductDefinition
{
	public Guid Id;

	public string Name = "";

	public string MenuText = "";

	/// <summary>When true, installing prompts for resolution + roles and only selected-role entries are registered.</summary>
	public bool HostTypeDialog;

	public List<LaunchEntryDefinition> Entries = new List<LaunchEntryDefinition>();
}

/// <summary>
/// Data-driven catalog of installable products, loaded from RedPlanet\Apps.xml (next to the exe).
/// Replaces the products that were hardcoded in <see cref="SiteManagement"/>.
/// </summary>
public static class AppRegistry
{
	private static readonly List<ProductDefinition> mProducts = new List<ProductDefinition>();

	private static readonly Dictionary<Guid, ProductDefinition> mById = new Dictionary<Guid, ProductDefinition>();

	public static IReadOnlyList<ProductDefinition> Products => mProducts;

	public static string CatalogPath =>
		Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "RedPlanet\\Apps.xml");

	static AppRegistry()
	{
		Reload();
	}

	public static ProductDefinition GetProduct(Guid id)
	{
		return mById.TryGetValue(id, out var product) ? product : null;
	}

	/// <summary>(Re)loads the catalog from <see cref="CatalogPath"/>. Safe if the file is missing — yields an empty catalog.</summary>
	public static void Reload()
	{
		LoadFromFile(CatalogPath);
	}

	/// <summary>Loads the catalog from an explicit path. Safe if the file is missing — yields an empty catalog.</summary>
	public static void LoadFromFile(string path)
	{
		mProducts.Clear();
		mById.Clear();
		if (!File.Exists(path))
		{
			return;
		}
		XmlDocument doc = new XmlDocument();
		doc.Load(path);
		foreach (XmlNode node in doc.DocumentElement.ChildNodes)
		{
			if (node.NodeType == XmlNodeType.Element && node.Name == "Product")
			{
				ProductDefinition product = ParseProduct(node);
				mProducts.Add(product);
				mById[product.Id] = product;
			}
		}
	}

	/// <summary>Adds a product to the catalog and persists the whole catalog to disk.</summary>
	public static void AddProduct(ProductDefinition product)
	{
		mProducts.Add(product);
		mById[product.Id] = product;
		Save();
	}

	/// <summary>Writes the catalog back to <see cref="CatalogPath"/>.</summary>
	public static void Save()
	{
		XmlDocument doc = new XmlDocument();
		doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null));
		doc.AppendChild(doc.CreateComment(
			" Tesla Console product catalog. Managed by Site Management (Add Product...). "));
		XmlElement root = doc.CreateElement("AppCatalog");
		doc.AppendChild(root);
		foreach (ProductDefinition product in mProducts)
		{
			XmlElement pe = doc.CreateElement("Product");
			pe.SetAttribute("id", product.Id.ToString("D"));
			pe.SetAttribute("name", product.Name);
			pe.SetAttribute("menuText", product.MenuText);
			pe.SetAttribute("hostTypeDialog", product.HostTypeDialog ? "true" : "false");
			foreach (LaunchEntryDefinition entry in product.Entries)
			{
				XmlElement le = doc.CreateElement("Launch");
				le.SetAttribute("key", entry.LaunchKey.ToString("D"));
				le.SetAttribute("displayName", entry.DisplayName);
				le.SetAttribute("exe", entry.Exe);
				le.SetAttribute("args", entry.Args ?? "");
				if (!string.IsNullOrEmpty(entry.WorkingDirectory))
				{
					le.SetAttribute("workingDirectory", entry.WorkingDirectory);
				}
				le.SetAttribute("autoRestart", entry.AutoRestart ? "true" : "false");
				le.SetAttribute("hostType", entry.HostType.ToString());
				pe.AppendChild(le);
			}
			root.AppendChild(pe);
		}
		Directory.CreateDirectory(Path.GetDirectoryName(CatalogPath));
		doc.Save(CatalogPath);
	}

	private static ProductDefinition ParseProduct(XmlNode node)
	{
		ProductDefinition product = new ProductDefinition
		{
			Id = ParseGuid(Attr(node, "id")),
			Name = Attr(node, "name"),
			MenuText = Attr(node, "menuText"),
			HostTypeDialog = ParseBool(Attr(node, "hostTypeDialog"))
		};
		if (string.IsNullOrEmpty(product.MenuText))
		{
			product.MenuText = product.Name + "...";
		}
		foreach (XmlNode child in node.ChildNodes)
		{
			if (child.NodeType == XmlNodeType.Element && child.Name == "Launch")
			{
				product.Entries.Add(ParseLaunch(child));
			}
		}
		return product;
	}

	private static LaunchEntryDefinition ParseLaunch(XmlNode node)
	{
		return new LaunchEntryDefinition
		{
			LaunchKey = ParseGuid(Attr(node, "key")),
			DisplayName = Attr(node, "displayName"),
			Exe = Attr(node, "exe"),
			Args = Attr(node, "args"),
			WorkingDirectory = HasAttr(node, "workingDirectory") ? Attr(node, "workingDirectory") : null,
			AutoRestart = !HasAttr(node, "autoRestart") || ParseBool(Attr(node, "autoRestart")),
			HostType = ParseHostType(Attr(node, "hostType"))
		};
	}

	private static bool HasAttr(XmlNode node, string name)
	{
		return node.Attributes?[name] != null;
	}

	private static string Attr(XmlNode node, string name)
	{
		return node.Attributes?[name]?.InnerText ?? "";
	}

	private static Guid ParseGuid(string value)
	{
		return Guid.TryParse(value, out var guid) ? guid : Guid.Empty;
	}

	private static bool ParseBool(string value)
	{
		return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || value == "1";
	}

	private static AppHostType ParseHostType(string value)
	{
		return Enum.TryParse<AppHostType>(value, ignoreCase: true, out var result) ? result : AppHostType.None;
	}
}
