using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using NetSparkleUpdater.Interfaces;
using NetSparkleUpdater.Enums;
using System.Threading;
using System.Collections.Generic;
using NetSparkleUpdater.Events;
using NetSparkleUpdater.AppCastHandlers;

namespace NetSparkleUpdater.UI.WinForms
{
    /// <summary>
    /// The main form
    /// </summary>
    public partial class UpdateAvailableWindow : Form, IUpdateAvailable
    {
        private readonly List<AppCastItem> _updates;
        private System.Windows.Forms.Timer? _ensureDialogShownTimer;
        private string _releaseNotesHTMLTemplate;
        private string _additionalReleaseNotesHeaderHTML;
        private string _releaseNotesDateFormat;

        /// <summary>
        /// Template for HTML code drawing release notes separator. {0} used for version number, {1} for publication date
        /// </summary>
        private CancellationToken? _cancellationToken;
        private CancellationTokenSource? _cancellationTokenSource;

        private bool _didSendResponse = false;


        /// <summary>
        /// Event fired when the user has responded to the 
        /// skip, later, install question.
        /// </summary>
        public event UserRespondedToUpdate? UserResponded;

        /// <summary>
        /// Object responsible for downloading and formatting markdown release notes for display in HTML
        /// </summary>
        public ReleaseNotesGrabber? ReleaseNotesGrabber { get; set; }

        private ISignatureVerifier? _signatureVerifier;

        /// <summary>
        /// Form constructor for showing release notes.
        /// </summary>
        /// <param name="signatureVerifier">The <seealso cref="ISignatureVerifier"/> for verifying release note signatures</param>
        /// <param name="items">The list of <seealso cref="AppCastItem"/> updates that are available for the user</param>
        /// <param name="isUpdateAlreadyDownloaded">Whether or not the update is already downloaded ot the user's computer</param>
        /// <param name="releaseNotesHTMLTemplate">The HTML string template to show for the release notes</param>
        /// <param name="additionalReleaseNotesHeaderHTML">The HTML string to add into the head element of the HTML for the release notes</param>
        /// <param name="releaseNotesDateFormat">Date format for release notes</param>
        /// <param name="appNameTitle">Title for application</param>
        /// <param name="installedVersion">Currently installed version of application</param>
        /// <param name="applicationIcon">The icon to display</param>
        public UpdateAvailableWindow(List<AppCastItem> items, ISignatureVerifier? signatureVerifier, bool isUpdateAlreadyDownloaded = false,
            string releaseNotesHTMLTemplate = "", string additionalReleaseNotesHeaderHTML = "", string releaseNotesDateFormat = "D",
            string appNameTitle = "the application", string installedVersion = "", Icon? applicationIcon = null)
        {
            _updates = items;
            _releaseNotesHTMLTemplate = releaseNotesHTMLTemplate;
            _additionalReleaseNotesHeaderHTML = additionalReleaseNotesHeaderHTML;
            _releaseNotesDateFormat = releaseNotesDateFormat;
            _signatureVerifier = signatureVerifier;

            InitializeComponent();

            var didError = false; // while loading release notes browser; if we have an error, make sure user sees it so dev can fix whatever happened
            try
            {
                ReleaseNotesBrowser.AllowWebBrowserDrop = false;
                ReleaseNotesBrowser.AllowNavigation = false;
            }
            catch (Exception ex)
            {
                lblInfoText.Text = string.Format("Error in browser init: {0}", ex.Message);
                didError = true;
            }

            AppCastItem? item = items.FirstOrDefault();

            var downloadInstallText = isUpdateAlreadyDownloaded ? "install" : "download";
            lblHeader.Text = lblHeader.Text.Replace("APP", item != null ? appNameTitle : "the application");
            if (!didError)
            {
                if (item != null)
                {
                    lblInfoText.Text = string.Format("{0} {1} is now available (you have {2}). Would you like to {3} it now?",
                        appNameTitle, item.Version, installedVersion, downloadInstallText);
                }
                else
                {
                    // TODO: string translations (even though I guess this window should never be called with 0 app cast items...)
                    lblInfoText.Text = string.Format("Would you like to {0} it now?", downloadInstallText);
                }
            }

            bool isUserMissingCriticalUpdate = items.Any(x => x.IsCriticalUpdate);
            buttonRemind.Enabled = isUserMissingCriticalUpdate == false;
            skipButton.Enabled = isUserMissingCriticalUpdate == false;
            //if (isUserMissingCriticalUpdate)
            //{
            //    FormClosing += UpdateAvailableWindow_FormClosing; // no closing a critical update!
            //}

            if (applicationIcon != null)
            {
                using (Icon icon = new Icon(applicationIcon, new Size(48, 48)))
                {
                    imgAppIcon.Image = icon.ToBitmap();
                }
                Icon = applicationIcon;
            }
            _cancellationTokenSource = new CancellationTokenSource();
            _cancellationToken = _cancellationTokenSource.Token;

            FormClosing += UpdateAvailableWindow_FormClosing;
        }

        /// <summary>
        /// Setup the ReleaseNotesGrabber (if needed) and load the release notes
        /// </summary>
        public void Initialize()
        {
            if (ReleaseNotesGrabber == null)
            {
                ReleaseNotesGrabber = new ReleaseNotesGrabber(_releaseNotesHTMLTemplate, _additionalReleaseNotesHeaderHTML, _signatureVerifier)
                {
                    DateFormat = _releaseNotesDateFormat
                };
            }
            ReleaseNotesBrowser.DocumentText = ReleaseNotesGrabber.GetLoadingText();
            EnsureDialogShown();
            LoadReleaseNotes(_updates);
        }

        private async void LoadReleaseNotes(List<AppCastItem> items)
        {
            AppCastItem? latestVersion = items.OrderByDescending(p => p.Version).FirstOrDefault();
            string releaseNotes = ReleaseNotesGrabber != null
                ? await ReleaseNotesGrabber.DownloadAllReleaseNotes(
                    items,
                    latestVersion ?? new AppCastItem(),
                    _cancellationToken ?? new CancellationTokenSource().Token)
                : "";
            ReleaseNotesBrowser.Invoke((MethodInvoker)delegate
            {
                // see https://stackoverflow.com/a/15209861/3938401
                ReleaseNotesBrowser.Navigate("about:blank");
                ReleaseNotesBrowser.Document?.OpenNew(true);
                ReleaseNotesBrowser.Document?.Write(releaseNotes);
                ReleaseNotesBrowser.DocumentText = releaseNotes;
            });
        }

        private void UpdateAvailableWindow_FormClosing(object? sender, FormClosingEventArgs e)
        {
            if (!_didSendResponse)
            {
                // user closed form in some other way other than standard buttons
                DialogResult = DialogResult.None;
                _didSendResponse = true;
                UserResponded?.Invoke(this, new UpdateResponseEventArgs(UpdateAvailableResult.None, CurrentItem));
            }
            FormClosing -= UpdateAvailableWindow_FormClosing;
        }

        /// <summary>
        /// The current item being installed
        /// </summary>
        AppCastItem IUpdateAvailable.CurrentItem => CurrentItem;

        /// <summary>
        /// The item that is being shown as the available update
        /// </summary>
        public AppCastItem CurrentItem
        {
            get { return _updates.Count > 0 ? _updates[0] : new AppCastItem(); }
        }

        /// <summary>
        /// The result of ShowDialog()
        /// </summary>
        UpdateAvailableResult IUpdateAvailable.Result => UIFactory.ConvertDialogResultToUpdateAvailableResult(DialogResult);

        /// <summary>
        /// Hides the release notes
        /// </summary>
        void IUpdateAvailable.HideReleaseNotes()
        {
            RemoveReleaseNotesControls();
        }

        /// <summary>
        /// Shows the dialog
        /// </summary>
        void IUpdateAvailable.Show()
        {
            if (InvokeRequired)
            {
                Invoke((MethodInvoker)delegate () { Show(); });
            }
            else
            {
                Show();
            }
            //if (!IsOnMainThread)
            //{
            //    Application.Run(this);
            //}
        }

        void IUpdateAvailable.BringToFront()
        {
            BringToFront();
        }

        void IUpdateAvailable.Close()
        {
            _cancellationTokenSource?.Cancel();
            CloseForm();
        }

        private void CloseForm()
        {
            if (InvokeRequired && !IsDisposed && !Disposing)
            {
                this.Invoke((MethodInvoker)delegate ()
                {
                    if (!IsDisposed && !Disposing)
                    {
                        Close();
                    }
                });
            }
            else if (!IsDisposed && !Disposing)
            {
                Close();
            }
        }

        /// <summary>
        /// Removes the release notes control
        /// </summary>
        public void RemoveReleaseNotesControls()
        {
            if (label3.Parent == null)
                return;

            // calc new size
            Size newSize = new Size(Size.Width, Size.Height - label3.Height - ReleaseNotesBrowser.Height);

            // remove the no more needed controls            
            label3.Parent?.Controls.Remove(label3);
            ReleaseNotesBrowser.Parent?.Controls.Remove(ReleaseNotesBrowser);

            // resize the window
            /*this.MinimumSize = newSize;
            this.Size = this.MinimumSize;
            this.MaximumSize = this.MinimumSize;*/
            Size = newSize;
        }

        void SendResponse(UpdateAvailableResult response)
        {
            _cancellationTokenSource?.Cancel();
            _didSendResponse = true;
            UserResponded?.Invoke(this, new UpdateResponseEventArgs(response, CurrentItem));
        }

        /// <summary>
        /// Event called when the skip button is clicked
        /// </summary>
        /// <param name="sender">not used.</param>
        /// <param name="e">not used.</param>
        private void OnSkipButtonClick(object? sender, EventArgs e)
        {
            // set the dialog result to no
            DialogResult = DialogResult.No;

            // close the windows
            SendResponse(UpdateAvailableResult.SkipUpdate);
        }

        /// <summary>
        /// Event called when the "remind me later" button is clicked
        /// </summary>
        /// <param name="sender">not used.</param>
        /// <param name="e">not used.</param>
        private void OnRemindClick(object sender, EventArgs e)
        {
            // set the dialog result ot retry
            DialogResult = DialogResult.Retry;

            // close the window
            SendResponse(UpdateAvailableResult.RemindMeLater);
        }

        /// <summary>
        /// Called when the "Update button" is clicked
        /// </summary>
        /// <param name="sender">not used.</param>
        /// <param name="e">not used.</param>
        private void OnUpdateButtonClick(object sender, EventArgs e)
        {
            // set the result to yes
            DialogResult = DialogResult.Yes;

            // close the dialog
            SendResponse(UpdateAvailableResult.InstallUpdate);
        }

        /// <summary>
        /// This was the only way Deadpikle could guarantee that the 
        /// update available window was shown above a main WPF MahApps window.
        /// It's an ugly hack but...oh well. :\
        /// </summary>
        public void EnsureDialogShown()
        {
            _ensureDialogShownTimer = new System.Windows.Forms.Timer();
            _ensureDialogShownTimer.Tick += new EventHandler(EnsureDialogeShown_tick);
            _ensureDialogShownTimer.Interval = 250; // in milliseconds
            _ensureDialogShownTimer.Start();
        }

        private void EnsureDialogeShown_tick(object sender, EventArgs e)
        {
            // http://stackoverflow.com/a/4831839/3938401 for activating/bringing to front code
            Activate();
            TopMost = true;  // important
            TopMost = false; // important
            Focus();         // important
            if (_ensureDialogShownTimer != null)
            {
                _ensureDialogShownTimer.Enabled = false;
            }
            _ensureDialogShownTimer = null;
        }

        /// <summary>
        /// Hides the remind me later button for the update available window
        /// </summary>
        public void HideRemindMeLaterButton()
        {
            buttonRemind.Visible = false;
        }

        /// <summary>
        /// Hides the skip button for the update available window
        /// </summary>
        public void HideSkipButton()
        {
            skipButton.Visible = false;
        }
    }
}