#region ======================== Namespaces

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using System.Windows.Forms;
using SharpCompress.Archives;
using SharpCompress.Readers;
using Microsoft.Win32;

#endregion

namespace mxd.GZDBUpdater
{
    public partial class MainForm : Form
	{
		#region ======================== Variables

		private string processToEnd = string.Empty;
		private string downloadFile = string.Empty;
        private string platform = string.Empty;
		private const string revisionwildcard = "[REVNUM]";
        private const string platformwildcard = "[PLATFORM]";
		private string URL = string.Empty;
        private readonly string updateFolder = Application.StartupPath + @"\_update\";
		private string appFileName = string.Empty;
	    private static BackgroundWorker worker;
	    private static bool appclosing;
	    private static MainForm me;
		private const string MESSAGEBOX_TITLE = "Ultimate Doom Builder Updater";
		private bool useInstaller = false;

		#endregion

		#region ======================== Delegates

		private delegate void SetLabelCallback(Label label, string text);
		private delegate void UpdateProgressBarCallback(ByteArgs args, int step, int totalsteps);
		private delegate void CloseDelegate();

		#endregion

		#region ======================== Properties

		public static string ErrorDescription;
	    public static bool AppClosing { get { return appclosing; } }
	    public static Icon AppIcon { get { return me.Icon; } }

	    #endregion

		#region ======================== Constructor

		public MainForm()
        {
			// Check if we should use the installer or just unpack the update
			string installLocation = (string)Registry.GetValue(@"HKEY_CURRENT_USER\SOFTWARE\Ultimate Doom Builder", "Location", null);

			if (installLocation != null)
			{
				string ourpath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
				if (ourpath == installLocation)
					useInstaller = true;
			}

			if (!CheckPremissions(Application.StartupPath))
            {
                ErrorDescription = "Update failed: your user account does not have write access to the destination folder \"" + Application.StartupPath + "\"\n\nMove the editor to a folder with write access,\nor run the updater as Administrator.";
                InvokeClose();
            }
            else if(!File.Exists("Updater.ini"))
			{
				ErrorDescription = "Unable to locate 'Updater.ini'...";
				InvokeClose();
			}
            else if(!LoadConfig("Updater.ini"))
			{
				InvokeClose();
			}
    		else
			{
				me = this;
				InitializeComponent();
			}
		}

		#endregion

		#region ======================== Updater thread

	    private void BackgroundWorker(object sender, DoWorkEventArgs e)
        {
			UpdateLabel(label1, "1/6: Checking revisions...");
			if(!UpdateRequired())
			{
				e.Cancel = true;
				return;
			}
            PreDownload();

			UpdateLabel(label1, "2/6: Downloading Update...");
			Webdata.BytesDownloaded += WebdataOnBytesDownloaded;
            if(!Webdata.SaveWebFile(URL, downloadFile, updateFolder))
            {
                e.Cancel = true;
				Webdata.BytesDownloaded -= WebdataOnBytesDownloaded;
				return;
            }

			// Check if the editor is running...
			if(!EditorClosed())
			{
				// Error or user canceled
				e.Cancel = true;
				return;
			}

			// Rename the updater so that a new version can be written
			UpdateLabel(label1, "4/6: Renaming updater...");
			Thread.Sleep(500);
			if(!RenameUpdater())
			{
				e.Cancel = true;
				return;
			}

			// Install the update
			if (useInstaller)
			{
				UpdateLabel(label1, "5/6: Running installer...");
				Thread.Sleep(500);
				if(!InstallUpdateWithInstaller())
				{
					e.Cancel = true;
					return;
				}
			}
			else
			{
				UpdateLabel(label1, "5/6: Decompressing package...");
				Thread.Sleep(500);
				if (!Unpack(updateFolder + downloadFile, Application.StartupPath))
				{
					e.Cancel = true;
					return;
				}
			}

			// This currently doesn't do anything, since the directory it tries to move the files
			// from is empty (bar the update file)
			/*
			UpdateLabel(label1, "5/6: Moving files...");
            Thread.Sleep(500);
            MoveFiles();
			*/
            
			UpdateLabel(label1, "6/6: Wrapping up...");
            Thread.Sleep(500);
			PostDownload();
		}

		private bool InstallUpdateWithInstaller()
		{
			Process process;

			try
			{
				// Run the installer in silent mode. This will skip any user interaction (if possible)
				process = Process.Start(updateFolder + downloadFile, "/silent");
			}
			catch (Exception ex)
			{
				ErrorDescription = $"Failed to run installer: {ex.Message}";
				return false;
			}

			process.WaitForExit();

			// Check if the installer ran successfully. See https://jrsoftware.org/ishelp/index.php?topic=setupexitcodes
			if (process.ExitCode == 2)
			{
				ErrorDescription = "Installer aborted by the user.";
				return false;
			}
			else if (process.ExitCode > 0)
			{
				ErrorDescription = $"Installer failed with exit code {process.ExitCode}.";
				return false;
			}

			return true;
		}

		private bool RenameUpdater()
		{
			string ourname = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);
			string movename = ourname + ".old";

			try
			{
				if (File.Exists(movename))
					File.Delete(movename);
				File.Move(ourname, movename);
			}
			catch(Exception e)
			{
				ErrorDescription = $"Failed to rename updater from {ourname} to {movename}: {e.Message}";
				return false;
			}

			return true;
		}

		private bool EditorClosed()
		{
			try
			{
				// Gather processes...
				List<Process> toclose = GetProcesses(processToEnd);

				// Ask the user how to proceed...
				if(toclose.Count > 0)
				{
					TaskbarProgress.SetState(this.Handle, TaskbarProgress.TaskbarStates.Paused);
					UpdateBlockedForm form = new UpdateBlockedForm();
					switch(form.ShowDialog(this))
					{
						case DialogResult.Cancel: return false;
						case DialogResult.OK:
							UpdateLabel(label1, "3/6: Stopping " + processToEnd);
							Thread.Sleep(50);
							toclose = GetProcesses(processToEnd); // Re-gather processes
							foreach(Process p in toclose) if(p != null) p.Kill();
							return true;
					}
				}
			}
			catch(Exception ex)
			{
				ErrorDescription = "Failed to stop the editor process...\n" + ex.Message;
				return false;
			}

			return true;
		}

		private static List<Process> GetProcesses(string processToEnd)
	    {
			Process[] processes = Process.GetProcesses();
			List<Process> toclose = new List<Process>();

			// Gather all running editor processes...
			foreach(Process process in processes)
			{
				if(process.ProcessName == processToEnd 
					&& Path.GetDirectoryName(process.MainModule.FileName) == Application.StartupPath)
					toclose.Add(process);
			}

			return toclose;
	    }

	    private static void StopBackgroundWorker()
		{
			if(worker != null && !worker.CancellationPending)
			{
				me.UpdateLabel(me.label1, "Stopping Background Thread...");
				worker.CancelAsync();
				while(worker.IsBusy) Application.DoEvents();
			}
		}

		private void WorkerOnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			InvokeClose();
		}

		#endregion

		#region ======================== Methods

		private void UpdateLabel(Label label, string text)
		{
			if(label.InvokeRequired)
			{
				SetLabelCallback d = UpdateLabel;
				label.Invoke(d, new object[] { label, text });
			}
			else
			{
				label.Text = text;
				label.Refresh();
				Invalidate();
			}
		}

		private void InvokeClose()
		{
			if(this.Disposing || this.IsDisposed) return;
			if(this.InvokeRequired)
			{
				CloseDelegate d = Close;
				this.Invoke(d);
			}
			else
			{
				if(!appclosing && !string.IsNullOrEmpty(ErrorDescription))
				{
					if(!string.IsNullOrEmpty(URL))
					{
						ErrorDescription += Environment.NewLine + Environment.NewLine + "Would you like to download the update manually?";
						TaskbarProgress.SetState(this.Handle, TaskbarProgress.TaskbarStates.Error);
						if(MessageBox.Show(this, ErrorDescription, MESSAGEBOX_TITLE, MessageBoxButtons.YesNo) == DialogResult.Yes)
							Process.Start(URL);
					}
					else
					{
						MessageBox.Show(this, ErrorDescription, MESSAGEBOX_TITLE, MessageBoxButtons.OK);
					}
				}

				WrapUp();
				Close();
			}
		}
		
		private bool UpdateRequired()
		{
			// Get local revision number
			int localrev = -1;
			if(File.Exists(appFileName))
			{
				var info = FileVersionInfo.GetVersionInfo(appFileName);
				localrev = info.ProductPrivatePart;
			}
			
			// Get remote revision number
			int remoterev;
			using(MemoryStream stream = Webdata.DownloadWebFile(Path.Combine(URL, "Versions.txt")))
			{
				if(stream == null)
				{
					if(string.IsNullOrEmpty(ErrorDescription)) ErrorDescription = "Failed to retrieve remote revision info.";
					return false;
				}

				string s;
				using(StreamReader reader = new StreamReader(stream))
				{
					s = reader.ReadLine(); // First line should be editor revision
				}

				if(!int.TryParse(s, out remoterev))
				{
					ErrorDescription = "Failed to retrieve remote revision number.";
					return false;
				}
			}

			// Replace wildcard with remoterev
			downloadFile = downloadFile.Replace(revisionwildcard, remoterev.ToString());
            downloadFile = downloadFile.Replace(platformwildcard, platform.ToString());

            if (remoterev > 0 && remoterev <= localrev)
			{
				URL = string.Empty;
				ErrorDescription = "Your version is up to date!";
			}
			return remoterev > localrev;
		}

		private bool LoadConfig(string filename)
		{
			string[] lines = File.ReadAllLines(filename);

			foreach (string line in lines)
			{
				if(line.StartsWith("URL"))
				{
					URL = line.Substring(3).Trim();
				} 
				else if(line.StartsWith("FileName"))
				{
					appFileName = line.Substring(8).Trim();
					processToEnd = Path.GetFileNameWithoutExtension(appFileName);
				}
				else if(!useInstaller && line.StartsWith("UpdateName"))
				{
					downloadFile = line.Substring(10).Trim();
				}
				else if(useInstaller && line.StartsWith("InstallerName"))
				{
					downloadFile = line.Substring(13).Trim();
				}
                else if(line.StartsWith("Platform"))
                {
                    platform = line.Substring(9).Trim();
                }
			}

			// Sanity cheks
			if(string.IsNullOrEmpty(URL))
			{
				ErrorDescription = "URL is not specified in " + filename + "!";
				return false;
			}

			if(string.IsNullOrEmpty(appFileName) || string.IsNullOrEmpty(processToEnd))
			{
				ErrorDescription = "FileName is not specified in " + filename + "!";
				return false;
			}

			if(string.IsNullOrEmpty(downloadFile) || !downloadFile.Contains(revisionwildcard))
			{
				ErrorDescription = "UpdateName is invalid or not specified in " + filename + "!";
				return false;
			}

			return true;
		}

        private bool Unpack(string file, string unZipTo)
        {
            try
            {
				using(IArchive arc = ArchiveFactory.Open(file))
				{
					if(!arc.IsComplete)
					{
						ErrorDescription = "Update failed: downloaded file is not complete...";
						return false;
					}

					IReader reader = arc.ExtractAllEntries();

					// Get number of files...
					int curentry = 0;
					int totalentries = arc.NumEntries;

					string ourname = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);

					// Unpack all
					ExtractionOptions options = new ExtractionOptions {ExtractFullPath = true, Overwrite = true};
					while(reader.MoveToNextEntry())
					{
						if(appclosing) break;
						// if(reader.Entry.IsDirectory || Path.GetFileName(reader.Entry.Key) == ourname) continue; // Don't try to overrite ourselves...
						reader.WriteEntryToDirectory(unZipTo, options);
						UpdateProgressBar(new ByteArgs { Downloaded = curentry++, Total = totalentries }, 1, 2);
					}
				}
            } 
			catch(Exception e) 
			{ 
				ErrorDescription = "Update failed: failed to unpack the update...\n" + e.Message;
				return false;
			}

	        return true;
        }

        private static bool CheckPremissions(string path)
        {
            try
            {
                DirectoryInfo di = new DirectoryInfo(path);
                DirectorySecurity acl = di.GetAccessControl();
                AuthorizationRuleCollection rules = acl.GetAccessRules(true, true, typeof(NTAccount));

                WindowsIdentity currentUser = WindowsIdentity.GetCurrent();
                WindowsPrincipal principal = new WindowsPrincipal(currentUser);
                foreach(AuthorizationRule rule in rules)
                {
                    FileSystemAccessRule fsAccessRule = rule as FileSystemAccessRule;
                    if(fsAccessRule == null) continue;

                    if((fsAccessRule.FileSystemRights & FileSystemRights.WriteData) > 0)
                    {
                        NTAccount ntAccount = rule.IdentityReference as NTAccount;
                        if(ntAccount == null) continue;
                        if(principal.IsInRole(ntAccount.Value)) return true;
                    }
                }
            }
            catch(UnauthorizedAccessException) { }

            return false;
        }

	    private void PreDownload()
        {
            if(!Directory.Exists(updateFolder)) Directory.CreateDirectory(updateFolder);
        }

        private void PostDownload()
        {
            if(!File.Exists(appFileName))
            {
	            ErrorDescription = "Unable to located updated executable ('" + appFileName + "')";
				return;
            }

			if(appclosing) return;
			Process.Start(new ProcessStartInfo { FileName = appFileName });
        }

        private void WrapUp()
        {
			if(Directory.Exists(updateFolder)) Directory.Delete(updateFolder, true);
        }

        private void MoveFiles()
        {
			DirectoryInfo di = new DirectoryInfo(updateFolder);
            FileInfo[] files = di.GetFiles();

            foreach(FileInfo fi in files)
            {
				if(fi.Name != downloadFile) File.Copy(updateFolder + fi.Name, Application.StartupPath + fi.Name, true);
            }
        }

		private void UpdateProgressBar(ByteArgs e, int step, int totalsteps)
        {
			if(progressbar.InvokeRequired)
			{
				UpdateProgressBarCallback d = UpdateProgressBar;
				progressbar.Invoke(d, new object[] { e, step, totalsteps });
			} 
			else 
			{
				int stepsize = (int)Math.Round((float)progressbar.Maximum / totalsteps);
				float ratio = (float)e.Downloaded / e.Total;
				int val = (int)Math.Floor(stepsize * step + stepsize * ratio);

				if(val <= progressbar.Maximum)
				{
					progressbar.Value = val;
					TaskbarProgress.SetValue(this.Handle, progressbar.Value, progressbar.Maximum);
				}
				progressbar.Refresh();
				Invalidate();
			}
		}

		#endregion

		#region ======================== Events

		private void MainForm_Load(object sender, EventArgs e)
		{
			Version version = Assembly.GetEntryAssembly().GetName().Version;
			this.Text += " v" + version.Major + "." + version.Revision.ToString("#00");

			worker = new BackgroundWorker();
			worker.DoWork += BackgroundWorker;
			worker.RunWorkerCompleted += WorkerOnRunWorkerCompleted;
			worker.WorkerSupportsCancellation = true;
			worker.RunWorkerAsync();
		}

		private void cancel_Click(object sender, EventArgs e)
		{
			ErrorDescription = string.Empty;
			appclosing = true;
			StopBackgroundWorker();
			InvokeClose();
		}

		private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
		{
			appclosing = true;
			StopBackgroundWorker();
		}

		private void WebdataOnBytesDownloaded(ByteArgs ba)
		{
			UpdateProgressBar(ba, 0, 2);
		}

		#endregion
	}
}