//
// Copyright 2020 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
// 
//   http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
//

using Google.Solutions.Apis.Auth;
using Google.Solutions.Common.Linq;
using Google.Solutions.Common.Util;
using Google.Solutions.IapDesktop.Application.Data;
using Google.Solutions.IapDesktop.Application.Diagnostics;
using Google.Solutions.IapDesktop.Application.Host;
using Google.Solutions.IapDesktop.Application.Profile;
using Google.Solutions.IapDesktop.Application.Profile.Settings;
using Google.Solutions.IapDesktop.Application.Theme;
using Google.Solutions.IapDesktop.Application.ToolWindows.ProjectExplorer;
using Google.Solutions.IapDesktop.Application.ToolWindows.Update;
using Google.Solutions.IapDesktop.Application.Windows;
using Google.Solutions.IapDesktop.Application.Windows.Auth;
using Google.Solutions.IapDesktop.Application.Windows.Dialog;
using Google.Solutions.IapDesktop.Application.Windows.Options;
using Google.Solutions.IapDesktop.Application.Windows.ProjectExplorer;
using Google.Solutions.IapDesktop.Core.ObjectModel;
using Google.Solutions.Mvvm.Binding;
using Google.Solutions.Mvvm.Binding.Commands;
using Google.Solutions.Mvvm.Controls;
using Google.Solutions.Mvvm.Drawing;
using Google.Solutions.Mvvm.Shell;
using Google.Solutions.Platform.Interop;
using Google.Solutions.Platform.Net;
using Google.Solutions.Settings.Collection;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using WeifenLuo.WinFormsUI.Docking;

#pragma warning disable IDE1006 // Naming Styles
#pragma warning disable CA1031 // Do not catch general exception types

namespace Google.Solutions.IapDesktop.Windows
{
    public partial class MainForm : Form, IJobHost, IMainWindow
    {
        //
        // Calculate minimum size so that it's a quarter of a 1080p screen,
        // leaving some room for the taskbar (typically <= 50px).
        //
        private static readonly Size MinimumWindowSize = new Size(
            1920 / 2,
            (1080 - 50) / 2);

        private readonly MainFormViewModel viewModel;

        private readonly IMainWindowTheme windowTheme;
        private readonly IDialogTheme dialogTheme;
        private readonly IRepository<IApplicationSettings> applicationSettings;
        private readonly IServiceProvider serviceProvider;
        private readonly IBindingContext bindingContext;

        private readonly ContextSource<IMainWindow> viewMenuContextSource;
        private readonly ContextSource<ToolWindowViewBase> windowMenuContextSource;

        private readonly CommandContainer<IMainWindow> viewMenuCommands;
        private readonly CommandContainer<ToolWindowViewBase> windowMenuCommands;

        public bool ShowWhatsNew { get; set; } = false;
        public IapRdpUrl? StartupUrl { get; set; }
        public ICommandContainer<IMainWindow> ViewMenu => this.viewMenuCommands;
        public ICommandContainer<ToolWindowViewBase> WindowMenu => this.windowMenuCommands;

        public MainForm(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;

            this.windowTheme = this.serviceProvider.GetService<IMainWindowTheme>();
            this.dialogTheme = this.serviceProvider.GetService<IDialogTheme>();
            this.applicationSettings = this.serviceProvider.GetService<IRepository<IApplicationSettings>>();
            this.bindingContext = serviceProvider.GetService<IBindingContext>();

            // 
            // Restore window settings.
            //
            var windowSettings = this.applicationSettings.GetSettings();
            if (windowSettings.IsMainWindowMaximized.Value)
            {
                this.WindowState = FormWindowState.Maximized;
                InitializeComponent();
            }
            else if (windowSettings.MainWindowHeight.Value != 0 &&
                     windowSettings.MainWindowWidth.Value != 0)
            {
                InitializeComponent();
                this.Size = new Size(
                    windowSettings.MainWindowWidth.Value,
                    windowSettings.MainWindowHeight.Value);
            }
            else
            {
                InitializeComponent();
            }

            SuspendLayout();

            this.windowTheme.ApplyTo(this);

            this.MinimumSize = MinimumWindowSize;

            // Set fixed size for the left/right panels (in pixels).
            this.dockPanel.DockLeftPortion =
                this.dockPanel.DockRightPortion = LogicalToDeviceUnits(300);

            //
            // View menu.
            //
            this.viewMenuContextSource = new ContextSource<IMainWindow>()
            {
                Context = this // Pseudo-context, never changes
            };

            this.viewMenuCommands = new CommandContainer<IMainWindow>(
                ToolStripItemDisplayStyle.ImageAndText,
                this.viewMenuContextSource,
                this.bindingContext);
            this.viewMenuCommands.BindTo(
                this.viewToolStripMenuItem,
                this.bindingContext);

            //
            // Window menu.
            //
            this.windowMenuContextSource = new ContextSource<ToolWindowViewBase>();

            this.windowToolStripMenuItem.DropDownOpening += (sender, args) =>
            {
                this.windowMenuContextSource.Context = this.dockPanel.ActiveContent as ToolWindowViewBase;
            };

            this.dockPanel.ActiveContentChanged += (sender, args) =>
            {
                //
                // NB. It's possible that ActiveContent is null although there
                // is an active document. Most commonly, this happens when
                // focus is released from an RDP window by using a keyboard
                // shortcut.
                //
                this.windowMenuContextSource.Context =
                    (this.dockPanel.ActiveContent ?? this.dockPanel.ActiveDocumentPane?.ActiveContent)
                        as ToolWindowViewBase;
            };

            this.windowMenuCommands = new CommandContainer<ToolWindowViewBase>(
                ToolStripItemDisplayStyle.ImageAndText,
                this.windowMenuContextSource,
                this.bindingContext);
            this.windowMenuCommands.BindTo(
                this.windowToolStripMenuItem,
                this.bindingContext);

            //
            // Bind controls.
            //
            this.viewModel = new MainFormViewModel(
                this,
                this.serviceProvider.GetService<IInstall>(),
                this.serviceProvider.GetService<UserProfile>(),
                this.serviceProvider.GetService<IAuthorization>());

            this.BindProperty(
                c => c.Text,
                this.viewModel,
                m => m.WindowTitle,
                this.bindingContext);

            //
            // Status bar.
            //
            this.statusStrip.BindReadonlyProperty(
                c => c.Active,
                this.viewModel,
                m => m.IsLoggingEnabled,
                this.bindingContext);
            this.toolStripStatus.BindProperty(
                c => c.Text,
                this.viewModel,
                m => m.StatusText,
                this.bindingContext);
            this.backgroundJobLabel.BindProperty(
                c => c.Visible,
                this.viewModel,
                m => m.IsBackgroundJobStatusVisible,
                this.bindingContext);
            this.cancelBackgroundJobsButton.BindProperty(
                c => c.Visible,
                this.viewModel,
                m => m.IsBackgroundJobStatusVisible,
                this.bindingContext);
            this.backgroundJobLabel.BindProperty(
                c => c.Text,
                this.viewModel,
                m => m.BackgroundJobStatus,
                this.bindingContext);
            this.profileStateButton.BindReadonlyProperty(
                c => c.Text,
                this.viewModel,
                m => m.ProfileStateCaption,
                this.bindingContext);

            //
            // Profile chooser.
            //
            var dynamicProfileMenuItemTag = new object();
            this.profileStateButton.DropDownOpening += (sender, args) =>
            {
                //
                // Re-populate list of profile menu items.
                //
                // Mark dynamic menu items with a tag so that we don't
                // accidentally remove any static menu items.
                //
                this.profileStateButton
                    .DropDownItems
                    .RemoveAll(item => item.Tag == dynamicProfileMenuItemTag)
                    .AddRange(this.viewModel
                        .AlternativeProfileNames
                        .Select(name => new ToolStripMenuItem(name)
                        {
                            Name = name,
                            Tag = dynamicProfileMenuItemTag
                        })
                        .ToArray());
            };

            this.profileStateButton.DropDownItemClicked += (sender, args) =>
            {
                if (args.ClickedItem.Tag == dynamicProfileMenuItemTag)
                {
                    this.viewModel.LaunchInstanceWithProfile(args.ClickedItem.Name);
                }
            };

            //
            // Logging.
            //
            this.enableloggingToolStripMenuItem.BindProperty(
                c => c.Checked,
                this.viewModel,
                m => m.IsLoggingEnabled,
                this.bindingContext);

            //
            // Bind menu commands.
            //
            this.WindowMenu.AddCommand(
                new ContextCommand<ToolWindowViewBase>(
                    "&Close",
                    window => window != null && window.IsDockable
                        ? CommandState.Enabled
                        : CommandState.Disabled,
                    window => window.CloseSafely()));
            this.WindowMenu.AddCommand(
                new ContextCommand<ToolWindowViewBase>(
                    "&Float",
                    window => window != null &&
                              !window.IsFloat &&
                              window.IsDockStateValid(DockState.Float)
                        ? CommandState.Enabled
                        : CommandState.Disabled,
                    window => window.IsFloat = true));
            this.WindowMenu.AddCommand(
                new ContextCommand<ToolWindowViewBase>(
                    "&Auto hide",
                    window => window != null && window.IsDocked && !window.IsAutoHide
                        ? CommandState.Enabled
                        : CommandState.Disabled,
                    window =>
                    {
                        window.IsAutoHide = true;
                        OnDockLayoutChanged();
                    })
                {
                    ShortcutKeys = Keys.Control | Keys.Alt | Keys.H
                });

            var dockCommand = this.WindowMenu.AddCommand(
                new ContextCommand<ToolWindowViewBase>(
                    "Dock",
                    _ => CommandState.Enabled,
                    context => { }));
            dockCommand.AddCommand(CreateDockCommand(
                "&Left",
                DockState.DockLeft,
                Keys.Control | Keys.Alt | Keys.Left));
            dockCommand.AddCommand(CreateDockCommand(
                "&Right",
                DockState.DockRight,
                Keys.Control | Keys.Alt | Keys.Right));
            dockCommand.AddCommand(CreateDockCommand(
                "&Top",
                DockState.DockTop,
                Keys.Control | Keys.Alt | Keys.Up));
            dockCommand.AddCommand(CreateDockCommand(
                "&Bottom",
                DockState.DockBottom,
                Keys.Control | Keys.Alt | Keys.Down));

            this.WindowMenu.AddSeparator();

            CommandState showTabCommand(ToolWindowViewBase window)
                => window != null && window.DockState == DockState.Document && window.Pane.Contents.Count > 1
                    ? CommandState.Enabled
                    : CommandState.Disabled;

            this.WindowMenu.AddCommand(
                new ContextCommand<ToolWindowViewBase>(
                    "&Next tab",
                    showTabCommand,
                    window => SwitchTab(window, 1))
                {
                    ShortcutKeys = Keys.Control | Keys.Alt | Keys.PageDown
                });
            this.WindowMenu.AddCommand(
                new ContextCommand<ToolWindowViewBase>(
                    "&Previous tab",
                    showTabCommand,
                    window => SwitchTab(window, -1))
                {
                    ShortcutKeys = Keys.Control | Keys.Alt | Keys.PageUp
                });
            this.WindowMenu.AddCommand(
                new ContextCommand<ToolWindowViewBase>(
                    "Capture/release &focus",
                    _ => this.dockPanel.ActiveDocumentPane != null &&
                         this.dockPanel.ActiveDocumentPane.Contents.EnsureNotNull().Any()
                        ? CommandState.Enabled
                        : CommandState.Disabled,
                    window => (this.dockPanel.ActiveDocumentPane?.ActiveContent as DocumentWindow)?.SwitchToDocument())
                {
                    ShortcutKeys = Keys.Control | Keys.Alt | Keys.Home
                });

            ResumeLayout();
        }

        //---------------------------------------------------------------------
        // Window events.
        //---------------------------------------------------------------------

        private void MainForm_FormClosing(object sender, FormClosingEventArgs _)
        {
            var settings = this.applicationSettings.GetSettings();

            //
            // Check for updates.
            //
            var checkForUpdates = new CheckForUpdateCommand<IMainWindow>(
                this,
                this.serviceProvider.GetService<IInstall>(),
                this.serviceProvider.GetService<IUpdatePolicy>(),
                this.serviceProvider.GetService<IReleaseFeed>(),
                this.serviceProvider.GetService<ITaskDialog>(),
                this.serviceProvider.GetService<IBrowser>());
            if (checkForUpdates.IsAutomatedCheckDue(
                DateTime.FromBinary(settings.LastUpdateCheck.Value)))
            {
                try
                {
                    using (var cts = new CancellationTokenSource())
                    {
                        //
                        // Check for updates. This check must be performed synchronously,
                        // otherwise this method returns and the application exits.
                        // In order not to block everything for too long in case of a network
                        // problem, use a timeout.
                        //
                        cts.CancelAfter(TimeSpan.FromSeconds(5));

                        //
                        // Prompt for survey unless the user has opted out.
                        //
                        checkForUpdates.EnableSurveys = settings.IsSurveyEnabled.Value;
                        if (Version.TryParse(
                            settings.LastSurveyVersion.Value,
                            out var lastSurveyVersion))
                        {
                            checkForUpdates.LastSurveyVersion = lastSurveyVersion;
                        }

                        checkForUpdates.Execute(cts.Token);

                        settings.LastUpdateCheck.Value = DateTime.UtcNow.ToBinary();
                        settings.IsSurveyEnabled.Value = checkForUpdates.EnableSurveys;
                        settings.LastSurveyVersion.Value = checkForUpdates.LastSurveyVersion?.ToString();
                    }
                }
                catch (Exception e)
                {
                    // Ignore in Release builds.
                    Debug.Fail(e.FullMessage());
                }
            }

            //
            // Save window state.
            //
            settings.IsMainWindowMaximized.Value = this.WindowState == FormWindowState.Maximized;
            settings.MainWindowHeight.Value = this.Size.Height;
            settings.MainWindowWidth.Value = this.Size.Width;

            this.applicationSettings.SetSettings(settings);
        }

        private void MainForm_Shown(object sender, EventArgs __)
        {
            var profile = this.serviceProvider.GetService<UserProfile>();
            if (!profile.IsDefault)
            {
                //
                // Add taskbar badge to help distinguish this profile
                // from other profiles.
                //
                // NB. This can only be done after the window has been shown,
                // so this code must not be moved to the constructor.
                //
                using (var badge = BadgeIcon.ForTextInitial(profile.Name))
                using (var taskbar = ComReference.For((ITaskbarList3)new TaskbarList()))
                {
                    taskbar.Object.HrInit();
                    taskbar.Object.SetOverlayIcon(
                        this.Handle,
                        badge.Handle,
                        string.Empty);
                }
            }

            if (this.StartupUrl != null)
            {
                //
                // Dispatch URL.
                //
                ConnectToUrl(this.StartupUrl);
            }
            else
            {
                //
                // No URL provided, just show project explorer then.
                //
                this.serviceProvider
                    .GetService<IToolWindowHost>()
                    .GetToolWindow<ProjectExplorerView, ProjectExplorerViewModel>()
                    .Show();
            }

            if (this.ShowWhatsNew)
            {
                //
                // Show the "What's new" window (in addition to the project explorer).
                //
                var window = this.serviceProvider
                    .GetService<IToolWindowHost>()
                    .GetToolWindow<ReleaseNotesView, ReleaseNotesViewModel>();
                window.ViewModel.ShowAllReleases = false;
                window.Show();
            }
        }

        private void SwitchTab(ToolWindowViewBase reference, int delta)
        {
            //
            // Find a sibling tab and activate it. Make sure
            // to not run out of bounds.
            //
            var pane = this.dockPanel.ActiveDocumentPane;
            var windowIndex = pane.Contents.IndexOf(reference);
            var tabCount = pane.Contents.Count;
            if (pane.Contents[(tabCount + windowIndex + delta) % tabCount] is DocumentWindow sibling)
            {
                sibling.SwitchToDocument();
            }
        }

        private ContextCommand<ToolWindowViewBase> CreateDockCommand(
            string caption,
            DockState dockState,
            Keys shortcutKeys)
        {
            return new ContextCommand<ToolWindowViewBase>(
                caption,
                window => window != null &&
                            window.VisibleState != dockState &&
                            window.IsDockStateValid(dockState)
                    ? CommandState.Enabled
                    : CommandState.Disabled,
                window =>
                {
                    window.DockState = dockState;
                    OnDockLayoutChanged();
                })
            {
                ShortcutKeys = shortcutKeys
            };
        }

        private void OnDockLayoutChanged()
        {
            //
            // The DockPanel has a quirk where re-docking a
            // window doesn't cause the document to re-paint,
            // even if it changed positions.
            // To fix this, force the active document pane
            // to relayout.
            //
            this.dockPanel.ActiveDocumentPane?.PerformLayout();

            //
            // Force context refresh. This is necessary
            // of the command is triggered by a shortcut,
            // bypassing the menu-open event (which normally
            // updates the context).
            //
            this.windowMenuCommands.ForceRefresh();
        }

        private async Task ConnectToUrlAsync(IapRdpUrl url)
        {
            var command = this.serviceProvider.GetService<UrlCommands>().LaunchRdpUrl;
            if (command.QueryState(url) == CommandState.Enabled)
            {
                try
                {
                    await command
                        .ExecuteAsync(url)
                        .ConfigureAwait(true);
                }
                catch (Exception e) when (e.IsCancellation())
                {
                    // The user cancelled, nervemind.
                }
                catch (Exception e)
                {
                    this.serviceProvider
                        .GetService<IExceptionDialog>()
                        .Show(
                            this,
                            $"Connecting to the VM instance {url.Instance.Name} failed", e);
                }
            }
        }

        internal void ConnectToUrl(IapRdpUrl url)
        {
            _ = ConnectToUrlAsync(url).ContinueWith(_ => { });
        }

        private void dockPanel_ActiveContentChanged(object sender, EventArgs e)
        {
            if (this.dockPanel.ActiveContent is ToolWindowViewBase toolWindow &&
                toolWindow.DockState == DockState.Document)
            {
                //
                // Focus switched to a document (we're not interested in
                // any other windows).
                //
                this.viewModel.SwitchToDocument(toolWindow.Text);
            }
            else if (
                this.dockPanel.ActiveContent == null ||
                !this.dockPanel.Documents.EnsureNotNull().Any())
            {
                //
                // All documents closed.
                //
                this.viewModel.SwitchToDocument(null);
            }
        }

        //---------------------------------------------------------------------
        // IMainForm.
        //---------------------------------------------------------------------

        public IWin32Window Window => this;
        public DockPanel MainPanel => this.dockPanel;

        public ICommandContainer<TContext> AddMenu<TContext>(
            string caption,
            int? index,
            Func<TContext?> queryCurrentContextFunc)
            where TContext : class
        {
            var menu = new ToolStripMenuItem(caption);

            if (index.HasValue)
            {
                this.mainMenu.Items.Insert(
                    Math.Min(index.Value, this.mainMenu.Items.Count),
                    menu);
            }
            else
            {
                this.mainMenu.Items.Add(menu);
            }

            var container = new CommandContainer<TContext>(
                ToolStripItemDisplayStyle.ImageAndText,
                new CallbackSource<TContext>(queryCurrentContextFunc),
                this.bindingContext);
            container.BindTo(menu, this.bindingContext);

            menu.DropDownOpening += (sender, args) =>
            {
                //
                // Force refresh since we can't know if the context
                // has changed or not.
                //
                container.ForceRefresh();
            };

            return container;
        }

        public void Minimize()
        {
            this.WindowState = FormWindowState.Minimized;
        }

        public bool IsWindowThread()
        {
            return !this.InvokeRequired;
        }

        //---------------------------------------------------------------------
        // Main menu events.
        //---------------------------------------------------------------------

        private void projectExplorerToolStripMenuItem_Click(object sender, EventArgs _)
        {
            this.serviceProvider
                .GetService<IToolWindowHost>()
                .GetToolWindow<ProjectExplorerView, ProjectExplorerViewModel>()
                .Show();
        }

        private void enableloggingToolStripMenuItem_Click(object sender, EventArgs _)
        {
            try
            {
                // Toggle logging state.
                this.viewModel.IsLoggingEnabled = !this.viewModel.IsLoggingEnabled;
            }
            catch (Exception e)
            {
                this.serviceProvider
                    .GetService<IExceptionDialog>()
                    .Show(this, "Configuring logging failed", e);
            }
        }

        private void optionsToolStripMenuItem_Click(object sender, EventArgs _)
        {
            try
            {
                OptionsDialog.Show(this, (IServiceCategoryProvider)this.serviceProvider);
            }
            catch (TaskCanceledException)
            {
                // Ignore.
            }
            catch (Exception e)
            {
                this.serviceProvider
                    .GetService<IExceptionDialog>()
                    .Show(this, "Opening Options window failed", e);
            }
        }

        private void accessStateButton_Click(object sender, EventArgs e)
        {
            var button = (ToolStripItem)sender;
            var screenPosition = new Rectangle(
                this.statusStrip.PointToScreen(button.Bounds.Location),
                button.Size);

            this.serviceProvider
                .GetService<WindowActivator<AccessInfoFlyoutView, AccessInfoViewModel, IMainWindowTheme>>()
                .CreateWindow()
                .Form
                .Show(this, screenPosition, ContentAlignment.TopLeft);
        }

        private void addProfileToolStripMenuItem_Click(object sender, EventArgs _)
        {
            try
            {
                using (var dialog = this.serviceProvider
                    .GetService<WindowActivator<NewProfileView, NewProfileViewModel, IDialogTheme>>()
                    .CreateDialog())
                {
                    if (dialog.ShowDialog(this) == DialogResult.OK)
                    {
                        using (var profile = this.serviceProvider
                            .GetService<IInstall>()
                            .CreateProfile(dialog.ViewModel.ProfileName))
                        {
                            this.viewModel.LaunchInstanceWithProfile(profile.Name);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                this.serviceProvider
                    .GetService<IExceptionDialog>()
                    .Show(this, "New profile", e);
            }
        }

        //---------------------------------------------------------------------
        // IJobHost.
        //---------------------------------------------------------------------

        public ISynchronizeInvoke Invoker => this;

        public IJobUserFeedback ShowFeedback(
            JobDescription jobDescription,
            CancellationTokenSource cancellationSource)
        {
            Debug.Assert(!this.Invoker.InvokeRequired, "ShowForegroundFeedback must be called on UI thread");

            switch (jobDescription.Feedback)
            {
                case JobUserFeedbackType.ForegroundFeedback:
                    //
                    // Show WaitDialog, blocking all user intraction.
                    //
                    var waitDialog = new WaitDialog(this, jobDescription.StatusMessage, cancellationSource);
                    this.dialogTheme.ApplyTo(waitDialog);
                    return waitDialog;

                default:
                    return this.viewModel.CreateBackgroundJob(jobDescription, cancellationSource);
            }
        }

        public void Reauthorize()
        {
            using (var dialog = this.serviceProvider
                .GetService<WindowActivator<AuthorizeView, AuthorizeViewModel, IDialogTheme>>()
                .CreateDialog())
            {
                dialog.ViewModel.UseExistingAuthorization(
                    this.serviceProvider.GetService<IAuthorization>());
                dialog.ShowDialog(this);
            }
        }

        private void cancelBackgroundJobsButton_Click(object sender, EventArgs e)
            => this.viewModel.CancelBackgroundJobs();

        //---------------------------------------------------------------------
        // Helper classes.
        //---------------------------------------------------------------------

        private class CallbackSource<TContext> : IContextSource<TContext>
            where TContext : class
        {
            private readonly Func<TContext?> queryCurrentContextFunc;

            public CallbackSource(Func<TContext?> queryCurrentContextFunc)
            {
                this.queryCurrentContextFunc = queryCurrentContextFunc;
            }

            public TContext? Context => this.queryCurrentContextFunc();
        }
    }
}
