/******************************************************************************/
/* SlunkCrypt, by LoRd_MuldeR <MuldeR2@GMX.de>                                */
/* This work has been released under the CC0 1.0 Universal license!           */
/******************************************************************************/

using Microsoft.Win32;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Media;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Shell;
using System.Windows.Threading;

using com.muldersoft.slunkcrypt.gui.ctrls;
using com.muldersoft.slunkcrypt.gui.process;
using com.muldersoft.slunkcrypt.gui.utils;

namespace com.muldersoft.slunkcrypt.gui
{
    public enum ModeOfOperation { Encrypt, Decrypt }

    public partial class SlunkCryptGUI : Window, INotifyBusyChanged
    {
        private enum Status { Default, Success, Failure }
        private delegate Task<bool> SlunkProcessor(string inputFile, string outputFile, string password, bool enableLegacyCompat);

        public event PropertyChangedEventHandler PropertyChanged;
        public const int MIN_PASSWD_LENGTH = 8, REC_PASSWD_LENGTH = 12, GEN_PASSWD_LENGTH = 24, MAX_PASSWD_LENGTH = 256, MAX_PATH = 259;

        private readonly ApplicationConfig m_config = new ApplicationConfig();
        private readonly Lazy<string> m_about = new Lazy<string>(CreateAboutText);
        private readonly Random m_random = new Random();
        private readonly ObservableCollection<string> m_logFile = new ObservableCollection<string>();
        private readonly string m_defaultStatusText;
        private readonly DispatcherTimer m_dispatcherTimer;
        private readonly ReadOnlyObservableCollection<string> m_logFileReadOnly;

        private volatile int m_isInitialized = 0;
        private volatile ModeOfOperation m_modeOfOperation = (ModeOfOperation)(-1);
        private volatile bool m_busyFlag = false, m_checksumError = false, m_processReceived = false, m_disableAnimation = false;
        private volatile SlunkCryptRunner m_processRunner = null;
        private uint? m_menuId_disableAnimation = null, m_menuId_enableExpertMode = null;

        public const string ASCII_CHARS = "!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~";

        // =============================================================================
        // Constructor
        // =============================================================================

        public SlunkCryptGUI()
        {
            InitializeComponent();
            m_defaultStatusText = Label_Status.Text;
            m_dispatcherTimer = new DispatcherTimer(DispatcherPriority.ApplicationIdle, Dispatcher);
            m_dispatcherTimer.Tick += DispatcherTimer_Tick;
            m_dispatcherTimer.Interval = TimeSpan.FromMilliseconds(331);
            m_logFileReadOnly = new ReadOnlyObservableCollection<string>(m_logFile);
            m_disableAnimation = m_config.DisableBusyIndicator;
        }

        // =============================================================================
        // Properties
        // =============================================================================

        public bool IsBusy
        {
            get
            {
                return m_busyFlag;
            }
            set
            {
                if (m_busyFlag != value)
                {
                    m_dispatcherTimer.IsEnabled = (m_busyFlag = value) && (!m_disableAnimation);
                    NotifyPropertyChanged("IsBusy");
                    NotifyPropertyChanged("IsBusyIndicatorVisible");
                }
            }
        }

        public bool IsBusyIndicatorVisible
        {
            get
            {
                return m_busyFlag && (!m_disableAnimation);
            }
        }

        public ReadOnlyObservableCollection<string> LogFile
        {
            get
            {
                return m_logFileReadOnly;
            }
        }

        // =============================================================================
        // Event handlers
        // =============================================================================

        protected override void OnContentRendered(EventArgs e)
        {
            base.OnContentRendered(e);
            if (Interlocked.CompareExchange(ref m_isInitialized, 1, 0) == 0)
            {
                TabControl.MinHeight = TabControl.MaxHeight = TabControl.ActualHeight;
                MinWidth = ActualWidth;
                MinHeight = MaxHeight = ActualHeight;
                CreateIndicatorElements();
                CreateSystemMenu();
                if (RenderMode.SoftwareOnly.Equals(RenderOptions.ProcessRenderMode))
                {
                    Hint_SoftwareRendering.Visibility = Visibility.Visible;
                }
                if (m_config.LegacyCompat)
                {
                    Checkbox_Encrypt_LegacyCompat.IsChecked = Checkbox_Decrypt_LegacyCompat.IsChecked = true;
                }
                this.DisableMaximizeButton();
                this.BringWindowToTop();
            }
        }

        private void Button_Encrypt_InputFile_Click(object sender, RoutedEventArgs e)
        {
            string fileName;
            if (!string.IsNullOrEmpty(fileName = BrowseForFile(Edit_Encrypt_InputFile.Text, false)))
            {
                Edit_Encrypt_InputFile.Text = fileName;
                Edit_Encrypt_OutputFile.Text = GenerateEncryptOutputFileName(fileName);
            }
        }

        private void Button_Encrypt_OutputFile_Click(object sender, RoutedEventArgs e)
        {
            string fileName;
            if (!string.IsNullOrEmpty(fileName = BrowseForFile(Edit_Encrypt_OutputFile.Text, true, "Encrypted file (*.enc)|*.enc")))
            {
                Edit_Encrypt_OutputFile.Text = fileName;
            }
        }

        private void Button_Decrypt_InputFile_Click(object sender, RoutedEventArgs e)
        {
            string fileName;
            if (!string.IsNullOrEmpty(fileName = BrowseForFile(Edit_Decrypt_InputFile.Text, false)))
            {
                Edit_Decrypt_InputFile.Text = fileName;
                Edit_Decrypt_OutputFile.Text = GenerateDecryptOutputFileName(fileName);
            }
        }

        private void Button_Decrypt_OutputFile_Click(object sender, RoutedEventArgs e)
        {
            string fileName;
            if (!string.IsNullOrEmpty(fileName = BrowseForFile(Edit_Decrypt_OutputFile.Text, true, "Decrypted file (*.out)|*.out")))
            {
                Edit_Decrypt_OutputFile.Text = fileName;
            }
        }

        private async void Button_GeneratePasswd_Click(object sender, RoutedEventArgs e)
        {
            using (BusyManager busy = new BusyManager(this))
            {
                Button_Encrypt_Toggle.IsChecked = true;
                Edit_Encrypt_Password.Password = "...";
                string password;
                if (!string.IsNullOrEmpty(password = await GeneratePassword()))
                {
                    Edit_Encrypt_Password.Password = password;
                }
            }
        }

        private async void Button_Start_Click(object sender, RoutedEventArgs e)
        {
            if (!IsBusy)
            {
                ResetKeyboardFocus(Button_Start);
                switch (GetModeOfOperation(TabControl.SelectedItem))
                {
                    case ModeOfOperation.Encrypt:
                        Debug.Assert(m_modeOfOperation == ModeOfOperation.Encrypt);
                        await ValidateInputFile(Edit_Encrypt_InputFile, Edit_Encrypt_OutputFile, Edit_Encrypt_Password, Checkbox_Encrypt_LegacyCompat, Encrypt, true);
                        break;
                    case ModeOfOperation.Decrypt:
                        Debug.Assert(m_modeOfOperation == ModeOfOperation.Decrypt);
                        await ValidateInputFile(Edit_Decrypt_InputFile, Edit_Decrypt_OutputFile, Edit_Decrypt_Password, Checkbox_Decrypt_LegacyCompat, Decrypt, false);
                        break;
                    default:
                        TabControl.SelectedIndex = GetTabIndexOf(m_modeOfOperation);
                        break;
                }
            }
        }

        private void Button_About_Click(object sender, RoutedEventArgs e)
        {
            if (!IsBusy)
            {
                MessageBox.Show(this, m_about.Value, "About...", MessageBoxButton.OK, MessageBoxImage.Information);
            }
        }

        private void Button_Exit_Click(object sender, RoutedEventArgs e)
        {
            if (!IsBusy)
            {
                Application.Current.Shutdown();
            }
        }

        private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            foreach (object currrentItem in e.AddedItems)
            {
                ModeOfOperation? modeOfOperation = GetModeOfOperation(currrentItem);
                if (modeOfOperation.HasValue)
                {
                    if (m_modeOfOperation != modeOfOperation.Value)
                    {
                        SetStatus(m_defaultStatusText);
                        ClearProgress();
                        m_modeOfOperation = modeOfOperation.Value;
                    }
                }
                break;
            }
        }

        private void Button_AbortProcess_Click(object sender, RoutedEventArgs e)
        {
            AbortProcess();
        }

        private void Link_CopyToClipboard_Click(object sender, MouseButtonEventArgs e)
        {
            if (m_logFile.Count > 0)
            {
                StringBuilder builder = new StringBuilder();
                foreach (string logItem in m_logFile)
                {
                    builder.AppendLine(logItem);
                }
                Clipboard.SetText(builder.ToString());
                SystemSounds.Beep.Play();
            }
        }

        private void Link_ClearLog_Click(object sender, MouseButtonEventArgs e)
        {
            if (m_logFile.Count > 0)
            {
                ClearLogFile();
                SystemSounds.Beep.Play();
            }
        }

        private void Process_OutputAvailable(string line, bool stderr)
        {
            AppendLogFile(line);
            if (line.IndexOf("Checksum mismatch detected!", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                m_checksumError = true;
            }
        }

        private void Porcess_ProgressChanged(double progress)
        {
            if (!m_processReceived)
            {
                switch (m_modeOfOperation)
                {
                    case ModeOfOperation.Encrypt:
                        SetStatus("Encrypting file contents. Please be patient, this can take a few moments...");
                        goto default;
                    case ModeOfOperation.Decrypt:
                        SetStatus("Decrypting file contents. Please be patient, this can take a few moments...");
                        goto default;
                    default:
                        m_processReceived = true;
                        break;
                }
            }
            SetProgress(progress);
        }

        private void Edit_FileName_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Return)
            {
                FrameworkElement source = sender as FrameworkElement;
                if (!ReferenceEquals(source, null))
                {
                    FrameworkElement target = source.Tag as FrameworkElement;
                    if (!ReferenceEquals(target, null))
                    {
                        SetFocusAndSelectAll(target);
                    }
                }
                e.Handled = true;
            }
        }

        private void Edit_Password_Entered(object sender, KeyEventArgs e)
        {
            if (!IsBusy)
            {
                Button_Start_Click(sender, e);
            }
        }

        private void Edit_FileName_LostFocus(object sender, RoutedEventArgs e)
        {
            TextBox textBox;
            if (!ReferenceEquals(textBox = sender as TextBox, null))
            {
                textBox.Text = PathUtils.CleanUpFilePathString(textBox.Text);
            }
        }

        private void Image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            DragMove();
        }

        private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Escape)
            {
                AbortProcess();
            }
        }

        protected void Window_PreviewDragEnter(object sender, DragEventArgs e)
        {
            e.Effects = ((!IsBusy) && e.Data.GetDataPresent(DataFormats.FileDrop)) ? DragDropEffects.Copy : DragDropEffects.None;
            e.Handled = true;
        }

        private void Window_PreviewDragLeave(object sender, DragEventArgs e)
        {
            e.Handled = true;
        }

        private void Window_PreviewDrop(object sender, DragEventArgs e)
        {
            if (!IsBusy)
            {
                string[] droppedFiles = e.Data.GetData(DataFormats.FileDrop) as string[];
                if (!ReferenceEquals(droppedFiles, null))
                {
                    foreach (string currentFile in droppedFiles)
                    {
                        string fullFilePath = PathUtils.CleanUpFilePathString(currentFile);
                        if ((!string.IsNullOrEmpty(fullFilePath)) && File.Exists(fullFilePath))
                        {
                            TabControl.SelectedIndex = GetTabIndexOf(m_modeOfOperation);
                            switch (m_modeOfOperation)
                            {
                                case ModeOfOperation.Encrypt:
                                    Edit_Encrypt_InputFile .Text = fullFilePath;
                                    Edit_Encrypt_OutputFile.Text = GenerateEncryptOutputFileName(fullFilePath);
                                    break;
                                case ModeOfOperation.Decrypt:
                                    Edit_Decrypt_InputFile .Text = fullFilePath;
                                    Edit_Decrypt_OutputFile.Text = GenerateDecryptOutputFileName(fullFilePath);
                                    break;
                            }
                            break;
                        }
                    }
                }
            }
            e.Handled = true;
        }

        private void Window_Closing(object sender, CancelEventArgs e)
        {
            if (IsBusy)
            {
                SystemSounds.Hand.Play();
                e.Cancel = true;
            }
        }

        private void DispatcherTimer_Tick(object sender, EventArgs e)
        {
            ShuffleIndicatorElements();
        }

        private void SystemMenu_Activated(SystemMenu sender, uint menuId)
        {
            if (m_menuId_disableAnimation.HasValue && (menuId == m_menuId_disableAnimation.Value))
            {
                sender.ModifyMenu(menuId, m_disableAnimation = !m_disableAnimation);
                if (m_busyFlag)
                {
                    m_dispatcherTimer.IsEnabled = !m_disableAnimation;
                    NotifyPropertyChanged("IsBusyIndicatorVisible");
                }
            }
            else if (m_menuId_enableExpertMode.HasValue && (menuId == m_menuId_enableExpertMode.Value))
            {
                try
                {
                    Process.Start("https://youtu.be/Is_8bjYVmnA").Dispose();
                }
                catch { }
            }
        }

        // =============================================================================
        // Internal methods
        // =============================================================================

        private async Task ValidateInputFile(TextBox inputFileEdit, TextBox outputFileEdit, PasswordToggleBox passwordEdit, CheckBox legacyCheckBox, SlunkProcessor processor, bool checkStrongPasswd)
        {
            string inputFilePath;
            if (string.IsNullOrEmpty(inputFileEdit.Text = inputFilePath = PathUtils.CleanUpFilePathString(inputFileEdit.Text)))
            {
                MessageBox.Show(this, "Input file must be selected first!", "Input File Missing", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(inputFileEdit);
                return;
            }
            if (PathUtils.IsInvalidPath(inputFilePath))
            {
                MessageBox.Show(this, "The specified input file path is invalid!", "Input File Invalid", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(inputFileEdit);
                return;
            }
            if (Directory.Exists(inputFilePath))
            {
                MessageBox.Show(this, "Specified input file appears to be a directory!", "Input File Invalid", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(inputFileEdit);
                return;
            }
            if (!File.Exists(inputFilePath))
            {
                MessageBox.Show(this, "Specified input file could not be found!", "Input Not Found", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(inputFileEdit);
                return;
            }
            await ValidateOutputFile(inputFilePath, outputFileEdit, passwordEdit, legacyCheckBox, processor, checkStrongPasswd);
        }

        private async Task ValidateOutputFile(string inputFilePath, TextBox outputFileEdit, PasswordToggleBox passwordEdit, CheckBox legacyCheckBox, SlunkProcessor processor, bool checkStrongPasswd)
        {
            string outputFilePath;
            if (string.IsNullOrEmpty(outputFileEdit.Text = outputFilePath = PathUtils.CleanUpFilePathString(outputFileEdit.Text)))
            {
                MessageBox.Show(this, "Output file must be selected first!", "Output File Missing", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(outputFileEdit);
                return;
            }
            if (PathUtils.IsInvalidPath(outputFilePath))
            {
                MessageBox.Show(this, "The specified output file path is invalid!", "Output File Invalid", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(outputFileEdit);
                return;
            }
            if (Directory.Exists(outputFilePath))
            {
                MessageBox.Show(this, "Specified output file appears to be a directory!", "Output File Invalid", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(outputFileEdit);
                return;
            }
            if (string.Equals(inputFilePath, outputFilePath, StringComparison.OrdinalIgnoreCase))
            {
                MessageBox.Show(this, "Input and output file can not be the same!", "File Name Conflict", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(outputFileEdit);
                return;
            }
            if (File.Exists(outputFilePath))
            {
                if (MessageBox.Show(this, "Specified output file already existst! Overwrite?", "Output File Exists", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) != MessageBoxResult.Yes)
                {
                    SetFocusAndSelectAll(outputFileEdit);
                    return;
                }
            }
            await ValidateOutputDirectory(inputFilePath, outputFilePath, passwordEdit, legacyCheckBox, processor, checkStrongPasswd);

        }

        private async Task ValidateOutputDirectory(string inputFilePath, string outputFilePath, PasswordToggleBox passwordEdit, CheckBox legacyCheckBox, SlunkProcessor processor, bool checkStrongPasswd)
        {
            string outputDirectory;
            if (string.IsNullOrEmpty(outputDirectory = PathUtils.TryGetDirectoryName(outputFilePath)))
            {
                MessageBox.Show(this, "The output directory could not be determined!", "Output Directory Invalid", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }
            while (!Directory.Exists(outputDirectory))
            {
                if (MessageBox.Show(this, "Output directory does not exist yet! Create it now?", "Output Directory Nonexistent", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.Yes) != MessageBoxResult.Yes)
                {
                    return;
                }
                if (!PathUtils.TryCreateDirectory(outputDirectory))
                {
                    MessageBox.Show(this, "The output directory could not be created!", "Directory Creation Failed", MessageBoxButton.OK, MessageBoxImage.Warning);
                    return;
                }
            }
            await ValidatePassword(inputFilePath, outputFilePath, passwordEdit, legacyCheckBox, processor, checkStrongPasswd);
        }

        private async Task ValidatePassword(string inputFilePath, string outputFilePath, PasswordToggleBox passwordEdit, CheckBox legacyCheckBox, SlunkProcessor processor, bool checkStrongPasswd)
        {
            bool enableLegacyCompat = legacyCheckBox.IsChecked.GetValueOrDefault();
            string passwordStr;
            if (string.IsNullOrEmpty(passwordStr = passwordEdit.Password) || (passwordStr.Length < MIN_PASSWD_LENGTH))
            {
                MessageBox.Show(this, string.Format("Passphrase must be at least {0:D} characters in length!", MIN_PASSWD_LENGTH), "Passphrase Missing", MessageBoxButton.OK, MessageBoxImage.Warning);
                SetFocusAndSelectAll(passwordEdit);
                return;
            }
            if (checkStrongPasswd)
            {
                if (passwordStr.Length < REC_PASSWD_LENGTH)
                {
                    if (MessageBox.Show(this, string.Format("Recommended passphrase length is at least {0:D} characters!", REC_PASSWD_LENGTH), "Short Passphrase", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel) != MessageBoxResult.OK)
                    {
                        SetFocusAndSelectAll(passwordEdit);
                        return;
                    }
                }
                else if (PasswordGen.IsWeakPassword(passwordStr))
                {
                    if (MessageBox.Show(this, "Passphrase should contain a mix of upper case characters, lower case characters, digits and other characters!", "Weak Passphrase", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel) != MessageBoxResult.OK)
                    {
                        SetFocusAndSelectAll(passwordEdit);
                        return;
                    }
                }
                if (enableLegacyCompat && (!m_config.LegacyCompat))
                {
                    if (MessageBox.Show(this, "Legacy compat-mode should not be used to encrypt new files!", "Legacy Compatibility", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel) != MessageBoxResult.OK)
                    {
                        legacyCheckBox.Focus();
                        return;
                    }
                }
            }
            await InvokeProcessor(inputFilePath, outputFilePath, passwordStr, processor, enableLegacyCompat);
        }

        private async Task InvokeProcessor(string inputFile, string outputFile, string password, SlunkProcessor processor, bool enableLegacyCompat)
        {
            using (BusyManager busyManager = new BusyManager(this))
            {
                ResetKeyboardFocus(this);
                SetProgress(double.PositiveInfinity);
                ClearLogFile();
                Button_Decrypt_Toggle.IsChecked = Button_Encrypt_Toggle.IsChecked = m_checksumError = m_processReceived = false;
                if (!await processor(inputFile, outputFile, password, enableLegacyCompat))
                {
                    if (!m_config.KeepIncompleteFiles)
                    {
                        PathUtils.TryRemoveFile(outputFile);
                    }
                    SetProgress(double.NaN, true);
                }
                await Task.Yield();
            }
        }

        private async Task<bool> Encrypt(string inputFile, string outputFile, string password, bool enableLegacyCompat)
        {
            SetStatus("Please wait while the encryption process is initializing...");
            int? exitCode = await RunProcess(SlunkCryptRunner.Mode.Encrypt, inputFile, outputFile, password, enableLegacyCompat);
            if (exitCode.HasValue)
            {
                if (exitCode.Value == 0)
                {
                    SetProgress(1);
                    SetStatus("Completed: The file has been encrypted successfully.", Status.Success);
                    SystemSounds.Asterisk.Play();
                }
                else
                {
                    SetProgress(1, true);
                    SetStatus("Error: Failed to enecrypt the file. Please see the log file for details!", Status.Failure);
                    SystemSounds.Hand.Play();
                }
                return true;
            }
            return false;
        }

        private async Task<bool> Decrypt(string inputFile, string outputFile, string password, bool enableLegacyCompat)
        {
            SetStatus("Please wait while the decryption process is initializing...");
            int? exitCode = await RunProcess(SlunkCryptRunner.Mode.Decrypt, inputFile, outputFile, password, enableLegacyCompat);
            if (exitCode.HasValue)
            {
                if (exitCode.Value == 0)
                {
                    SetStatus("Completed: The file has been decrypted successfully (checksum is correct).", Status.Success);
                    SetProgress(1);
                    SystemSounds.Asterisk.Play();
                }
                else
                {
                    if (m_checksumError)
                    {
                        SetStatus("Error: Checksum mismatch detected! Wrong passphrase or corrupted file?", Status.Failure);
                    }
                    else
                    {
                        SetStatus("Error: Failed to decrypt the file. Please see the log file for details!", Status.Failure);
                    }
                    SetProgress(1, true);
                    SystemSounds.Hand.Play();
                }
                return true;
            }
            return false;
        }

        private async Task<int?> RunProcess(SlunkCryptRunner.Mode mode, string inputFile, string outputFile, string password, bool enableLegacyCompat)
        {
            if (!ReferenceEquals(m_processRunner, null))
            {
                throw new InvalidOperationException("Process has already been started!");
            }
            try
            {
                SlunkCryptRunner.SlunkOptions options = new SlunkCryptRunner.SlunkOptions(m_config.KeepIncompleteFiles, m_config.ThreadCount, enableLegacyCompat);
                using (m_processRunner = new SlunkCryptRunner(Dispatcher))
                {
                    m_processRunner.OutputAvailable += Process_OutputAvailable;
                    m_processRunner.ProgressChanged += Porcess_ProgressChanged;
                    SetProcessPriority(ProcessPriorityClass.AboveNormal);
                    return await m_processRunner.ExecuteAsync(mode, inputFile, outputFile, password, options);
                }
            }
            catch (ProcessRunner.ProcessStartException err)
            {
                SetStatus(string.Format("Error: The {0} process could not be created! (Error code: {1:D})", GetModeString(m_modeOfOperation), GetWin32ErrorCode(err)), Status.Failure);
                MessageBox.Show(this, "Failed to create SlunkCrypt process:\n\n" + err.InnerException?.Message ?? err.Message, "Process Creation Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            catch (ExecutableHelper.ExecutableNotFoundException)
            {
                SetStatus("Error: The required SlunkCrypt executable file could not be found!", Status.Failure);
                MessageBox.Show(this, "The SlunkCrypt executable file could not be found.\n\nPlease make sure that the SlunkCrypt CLI executable file is located in the same directory as the GUI program!", "Executable Not Found", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            catch (ProcessRunner.ProcessInterruptedException)
            {
                SetStatus(string.Format("Aborted: The {0} process was aborted by the user!", GetModeString(m_modeOfOperation)), Status.Failure);
                SystemSounds.Hand.Play();
            }
            finally
            {
                m_processRunner = null; /*final clean-up*/
            }
            return null;
        }

        private async Task<string> GeneratePassword()
        {
            string password = string.Empty;
            try
            {
                Task<string> passwordTask = Task.Run(() => PasswordGen.GeneratePassword(GEN_PASSWD_LENGTH));
                await Task.WhenAll(passwordTask, Task.Delay(333));
                password = passwordTask.Result;
            }
            catch (ProcessRunner.ProcessStartException err)
            {
                MessageBox.Show(this, "Failed to create SlunkCrypt process:\n\n" + err.InnerException?.Message ?? err.Message, "Process Creation Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            catch (ExecutableHelper.ExecutableNotFoundException)
            {
                MessageBox.Show(this, "The SlunkCrypt executable file could not be found.\n\nPlease make sure that the SlunkCrypt CLI executable file is located in the same directory as the GUI program!", "Executable Not Found", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            catch (PasswordGen.GenerationFailedException)
            {
                MessageBox.Show(this, "Error: The password could not be generated!", "Generation Failed", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            return password;
        }

        private void SetStatus(string text, Status status = Status.Default)
        {
            switch (status)
            {
                case Status.Success:
                    Label_Status.Foreground = Brushes.DarkGreen;
                    break;
                case Status.Failure:
                    Label_Status.Foreground = Brushes.DarkRed;
                    break;
                default:
                    Label_Status.Foreground = SystemColors.WindowTextBrush;
                    break;
            }
            Label_Status.Text = text;
        }

        private void SetProgress(double progress, bool failed = false)
        {
            if (!(double.IsNaN(progress) || double.IsInfinity(progress)))
            {
                ProgressBar.IsIndeterminate = false;
                ProgressBar.Value = progress;
                TaskbarItemInfo.ProgressState = failed ? TaskbarItemProgressState.Error : TaskbarItemProgressState.Normal;
                TaskbarItemInfo.ProgressValue = progress;
                Label_Progress.Text = string.Format(CultureInfo.InvariantCulture, "{0:0.0}%", progress * 100.0);
            }
            else
            {
                if (double.IsInfinity(progress))
                {
                    ProgressBar.IsIndeterminate = true;
                    ProgressBar.Value = 0;
                    TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Indeterminate;
                    Label_Progress.Text = string.Empty;
                }
                else
                {
                    ProgressBar.IsIndeterminate = false;
                    TaskbarItemInfo.ProgressState = failed ? TaskbarItemProgressState.Error : TaskbarItemProgressState.None;
                }
            }
        }

        private void ClearProgress()
        {        
            ProgressBar.IsIndeterminate = false;
            ProgressBar.Value = 0;
            TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None;
            Label_Progress.Text = string.Empty;
        }

        private void AbortProcess()
        {
            ProcessRunner processRunner;
            if (!ReferenceEquals(processRunner = m_processRunner, null))
            {
                try
                {
                    processRunner.AbortProcess();
                }
                catch { }
            }
        }

        private string BrowseForFile(string fileName, bool saveDialog, string filterString = null)
        {
            FileDialog openFileDialog = saveDialog ? new SaveFileDialog() { OverwritePrompt = false } : (FileDialog) new OpenFileDialog();
            openFileDialog.Filter = string.IsNullOrEmpty(filterString) ? "All files (*.*)|*.*" : filterString;
            if (!string.IsNullOrEmpty(fileName))
            {
                openFileDialog.FileName = fileName;
            }
            if (openFileDialog.ShowDialog().GetValueOrDefault(false))
            {
                return openFileDialog.FileName;
            }
            return null;
        }

        private void CreateSystemMenu()
        {
            SystemMenu systemMenu = new SystemMenu(this, SystemMenu_Activated);
            m_menuId_disableAnimation = systemMenu.AppendMenu("Disable Busy Indicator");
            m_menuId_enableExpertMode = systemMenu.AppendMenu("Expert Settings");
            if (m_disableAnimation && m_menuId_disableAnimation.HasValue)
            {
                systemMenu.ModifyMenu(m_menuId_disableAnimation.Value, m_disableAnimation);
            }
        }

        private void CreateIndicatorElements()
        {
            FontFamily hackFont = new FontFamily(new Uri("pack://application:,,,/"), "./Resources/Fonts/#Hack");
            DropShadowEffect dropShadowEffect = CreateShadowEffect(Colors.Black, 3.0);
            TextBlock reference = CreateTextBlock('0', Brushes.Gold, hackFont, dropShadowEffect);
            reference.Measure(new Size(double.MaxValue, double.MaxValue));
            Size desiredSize = reference.DesiredSize;
            double actualWidth = Canvas.ActualWidth, actualHeight = Canvas.ActualHeight;
            int lenX = (int)Math.Ceiling(desiredSize.Width  * 1.25);
            int lenY = (int)Math.Ceiling(desiredSize.Height * 1.25);
            int numX = (int)Math.Floor(actualWidth  / lenX);
            int numY = (int)Math.Floor(actualHeight / lenY);
            int offX = (int)Math.Round((actualWidth  - (numX * lenX)) / 2.0);
            int offY = (int)Math.Round((actualHeight - (numY * lenY)) / 2.0);
            Canvas.Children.Clear();
            for (int i = 0; i < numX; ++i)
            {
                for (int j = 0; j < numY; ++j)
                {
                    TextBlock element = CreateTextBlock('0', Brushes.Gold, hackFont, dropShadowEffect);
                    Canvas.Children.Add(element);
                    Canvas.SetLeft(element, offX + (i * lenX));
                    Canvas.SetTop (element, offY + (j * lenY));
                }
            }
            ShuffleIndicatorElements();
        }

        private void ShuffleIndicatorElements()
        {
            char[] chars = ASCII_CHARS.ToCharArray();
            UIElementCollection children = Canvas.Children;
            for (int i = 0; i < children.Count; ++i)
            {
                TextBlock element;
                if (!ReferenceEquals(element = children[i] as TextBlock, null))
                {
                    if (m_random.Next(7) == 0)
                    {
                        element.Visibility = Visibility.Visible;
                        element.Text = char.ToString(chars[m_random.Next(chars.Length)]);
                    }
                    else
                    {
                        element.Visibility = Visibility.Hidden;
                    }
                }
            }
        }

        private void AppendLogFile(string line)
        {
            if (!string.IsNullOrEmpty(line))
            {
                m_logFile.Add(line);
            }
        }

        private int GetTabIndexOf(ModeOfOperation modeOfOperation)
        {
            ItemCollection collection = TabControl.Items;
            for (int index = 0; index < collection.Count; ++index)
            {
                ModeOfOperation? current = GetModeOfOperation(collection[index]);
                if (current.HasValue && (current.Value == modeOfOperation))
                {
                    return index;
                }
            }
            return -1;
        }

        private void ClearLogFile()
        {
            m_logFile.Clear();
        }

        private void NotifyPropertyChanged(string name)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }

        // -----------------------------------------------------------------------------
        // Static methods
        // -----------------------------------------------------------------------------

        private static string CreateAboutText()
        {
            CPUFeatures cpuFeatures = CPUFeatures.Features;
            return new StringBuilder()
                .AppendLine("SlunkCrypt, by LoRd_MuldeR <MuldeR2@GMX.de>")
                .AppendLine(VersionInfo.ToString())
                .AppendLine("This work has been released under the \u201CCC0 1.0\u201D license!")
                .AppendLine()
                .AppendLine("Official web-site: http://slunkcrypt.osdn.io/")
                .AppendLine()
                .AppendLine(Environment.OSVersion.VersionString)
                .AppendLine(string.Format("Operating System Bitness: {0:D}, Process Bitness: {1:D}", Environment.Is64BitOperatingSystem ? 64 : 32, Environment.Is64BitProcess ? 64 : 32))
                .AppendLine(".NET Runtime Version: " + Environment.Version)
                .AppendLine(string.Format("CPU Count: {0:D}, Architecture: {1}, SSE2: {2}, AVX2: {3}", Environment.ProcessorCount, cpuFeatures.cpuArch.GetDescription(), cpuFeatures.hasSSE2 ? "Yes" : "No", cpuFeatures.hasAVX2 ? "Yes" : "No"))
                .AppendLine()
                .AppendLine("Using “Silk” icons, by Mark James")
                .ToString();
        }

        private static string GenerateEncryptOutputFileName(string inputFilePath)
        {
            string directoryPath = Path.GetDirectoryName(inputFilePath), fileName = Path.GetFileNameWithoutExtension(inputFilePath), extension = Path.GetExtension(inputFilePath);
            string outputFile = Path.Combine(directoryPath, string.Format("{0}{1}.enc", fileName, extension));
            for (int count = 2; File.Exists(outputFile); ++count)
            {
                outputFile = Path.Combine(directoryPath, string.Format("{0} ({1:D}){2}.enc", fileName, count, extension));
            }
            return outputFile;
        }

        private static string GenerateDecryptOutputFileName(string inputFilePath)
        {
            string directoryPath = Path.GetDirectoryName(inputFilePath), fileName = Path.GetFileNameWithoutExtension(inputFilePath), extension = Path.GetExtension(inputFilePath);
            while (extension.Equals(".enc", StringComparison.OrdinalIgnoreCase))
            {
                extension = Path.GetExtension(fileName);
                fileName = Path.GetFileNameWithoutExtension(fileName);
            }
            if (string.IsNullOrEmpty(extension))
            {
                extension = ".out";
            }
            string outputFile = Path.Combine(directoryPath, string.Concat(fileName, extension));
            for (int count = 2; File.Exists(outputFile); ++count)
            {
                outputFile = Path.Combine(directoryPath, String.Format("{0} ({1:D}){2}", fileName, count, extension));
            }
            return outputFile;
        }

        private static ModeOfOperation? GetModeOfOperation(object selectedItem)
        {
            TabItem selectedTabItem = selectedItem as TabItem;
            if (!ReferenceEquals(selectedTabItem, null))
            {
                return selectedTabItem.Tag as ModeOfOperation?;
            }
            return null;
        }

        private static string GetModeString(ModeOfOperation modeOfOperation)
        {
            switch(modeOfOperation)
            {
                case ModeOfOperation.Encrypt:
                    return "encryption";
                case ModeOfOperation.Decrypt:
                    return "decryption";
                default:
                    throw new ArgumentException("modeOfOperation");
            }
        }

        private static void SetFocusAndSelectAll(FrameworkElement element)
        {
            TextBox textBox;
            if (!ReferenceEquals(textBox = element as TextBox, null))
            {
                textBox.Focus();
                textBox.SelectAll();
            }
            else
            {
                PasswordToggleBox passwordToggleBox;
                if (!ReferenceEquals(passwordToggleBox = element as PasswordToggleBox, null))
                {
                    passwordToggleBox.Focus();
                    passwordToggleBox.SelectAll();
                }
            }
        }

        private static void SetProcessPriority(ProcessPriorityClass priorityClass)
        {
            try
            {
                using (Process currentProcess = Process.GetCurrentProcess())
                {
                    currentProcess.PriorityClass = priorityClass;
                }
            }
            catch { }
        }

        private static int GetWin32ErrorCode(Exception err)
        {
            while (!ReferenceEquals(err, null))
            {
                if (err is Win32Exception)
                {
                    return ((Win32Exception)err).NativeErrorCode;
                }
                err = err.InnerException;
            }
            return 0;
        }

        private static TextBlock CreateTextBlock(char c, Brush foreground, FontFamily fontFamily, Effect effect)
        {
            return new TextBlock()
            {
                Text = char.ToString(c),
                Foreground = foreground,
                FontFamily = fontFamily,
                Effect = effect
            };
        }

        private static DropShadowEffect CreateShadowEffect(Color color, double blurRadius)
        {
            return new DropShadowEffect()
            {
                Color = color,
                BlurRadius = blurRadius,
                Direction = 0.0,
                ShadowDepth = 0.0
            };
        }

        private static void ResetKeyboardFocus(UIElement element)
        {
            Keyboard.ClearFocus();
            element.Focus();
        }
    }
}
