// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
// This file contains much of the code related to tab management for the
// TerminalPage. Things like opening new tabs, selecting different tabs,
// switching tabs, should all be handled in this file. Hypothetically, in the
// future, the contents of this file could be moved to a separate class
// entirely.
//

#include "pch.h"
#include "TerminalPage.h"
#include "Utils.h"
#include "../../types/inc/utils.hpp"
#include "../../inc/til/string.h"
#include <til/io.h>

#include "TabRowControl.h"
#include "DebugTapConnection.h"
#include "..\TerminalSettingsModel\FileUtils.h"
#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h"

#include <shlobj.h>

using namespace winrt;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::System;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
using namespace winrt::Windows::UI::Text;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Storage::Pickers;
using namespace winrt::Windows::Storage::Provider;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Control;
using namespace winrt::Microsoft::Terminal::TerminalConnection;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace ::TerminalApp;
using namespace ::Microsoft::Console;

namespace winrt
{
    namespace MUX = Microsoft::UI::Xaml;
    namespace WUX = Windows::UI::Xaml;
    using IInspectable = Windows::Foundation::IInspectable;
}

namespace winrt::TerminalApp::implementation
{
    // Method Description:
    // - Open a new tab. This will create the TerminalControl hosting the
    //   terminal, and add a new Tab to our list of tabs. The method can
    //   optionally be provided a NewTerminalArgs, which will be used to create
    //   a tab using the values in that object.
    // Arguments:
    // - newTerminalArgs: An object that may contain a blob of parameters to
    //   control which profile is created and with possible other
    //   configurations. See TerminalSettings::CreateWithNewTerminalArgs for more details.
    // - existingConnection: An optional connection that is already established to a PTY
    //   for this tab to host instead of creating one.
    //   If not defined, the tab will create the connection.
    HRESULT TerminalPage::_OpenNewTab(const INewContentArgs& newContentArgs)
    try
    {
        if (const auto& newTerminalArgs{ newContentArgs.try_as<NewTerminalArgs>() })
        {
            const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) };
            // GH#11114: GetProfileForArgs can return null if the index is higher
            // than the number of available profiles.
            if (!profile)
            {
                return S_FALSE;
            }
            const auto settings{ Settings::TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs) };

            // Try to handle auto-elevation
            if (_maybeElevate(newTerminalArgs, settings, profile))
            {
                return S_OK;
            }
            // We can't go in the other direction (elevated->unelevated)
            // unfortunately. This seems to be due to Centennial quirks. It works
            // unpackaged, but not packaged.
        }

        // This call to _MakePane won't return nullptr, we already checked that
        // case above with the _maybeElevate call.
        _CreateNewTabFromPane(_MakePane(newContentArgs, nullptr));
        return S_OK;
    }
    CATCH_RETURN();

    // Method Description:
    // - Sets up state, event handlers, etc on a tab object that was just made.
    // Arguments:
    // - newTabImpl: the uninitialized tab.
    // - insertPosition: Optional parameter to indicate the position of tab.
    void TerminalPage::_InitializeTab(winrt::com_ptr<Tab> newTabImpl, uint32_t insertPosition)
    {
        newTabImpl->Initialize();

        // If insert position is not passed, calculate it
        if (insertPosition == -1)
        {
            insertPosition = _tabs.Size();
            if (_settings.GlobalSettings().NewTabPosition() == NewTabPosition::AfterCurrentTab)
            {
                auto currentTabIndex = _GetFocusedTabIndex();
                if (currentTabIndex.has_value())
                {
                    insertPosition = currentTabIndex.value() + 1;
                }
            }
        }

        // Add the new tab to the list of our tabs.
        _tabs.InsertAt(insertPosition, *newTabImpl);
        _mruTabs.Append(*newTabImpl);

        newTabImpl->SetDispatch(*_actionDispatch);
        newTabImpl->SetActionMap(_settings.ActionMap());

        // Give the tab its index in the _tabs vector so it can manage its own SwitchToTab command.
        _UpdateTabIndices();

        // Hookup our event handlers to the new terminal
        _RegisterTabEvents(*newTabImpl);

        // Don't capture a strong ref to the tab. If the tab is removed as this
        // is called, we don't really care anymore about handling the event.
        auto weakTab = make_weak(newTabImpl);

        // When the tab's active pane changes, we'll want to lookup a new icon
        // for it. The Title change will be propagated upwards through the tab's
        // PropertyChanged event handler.
        newTabImpl->ActivePaneChanged({ get_weak(), &TerminalPage::_activePaneChanged });

        // The RaiseVisualBell event has been bubbled up to here from the pane,
        // the next part of the chain is bubbling up to app logic, which will
        // forward it to app host.
        newTabImpl->TabRaiseVisualBell([weakTab, weakThis{ get_weak() }]() {
            auto page{ weakThis.get() };
            auto tab{ weakTab.get() };

            if (page && tab)
            {
                page->RaiseVisualBell.raise(nullptr, nullptr);
            }
        });

        auto tabViewItem = newTabImpl->TabViewItem();
        _tabView.TabItems().InsertAt(insertPosition, tabViewItem);

        // Set this tab's icon to the icon from the content
        _UpdateTabIcon(*newTabImpl);

        tabViewItem.PointerPressed({ this, &TerminalPage::_OnTabPointerPressed });

        // When the tab requests close, try to close it (prompt for approval, if required)
        newTabImpl->CloseRequested([weakTab, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) {
            auto page{ weakThis.get() };
            auto tab{ weakTab.get() };

            if (page && tab)
            {
                page->_HandleCloseTabRequested(*tab);
            }
        });

        // When the tab is closed, remove it from our list of tabs.
        newTabImpl->Closed([weakTab, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) {
            const auto page = weakThis.get();
            const auto tab = weakTab.get();

            if (page && tab)
            {
                page->_RemoveTab(*tab);
            }
        });

        // The tab might want us to toss focus into the control, especially when
        // transient UIs (like the context menu, or the renamer) are dismissed.
        newTabImpl->RequestFocusActiveControl([weakThis{ get_weak() }]() {
            if (const auto page{ weakThis.get() })
            {
                page->_FocusCurrentTab(false);
            }
        });

        // This kicks off TabView::SelectionChanged, in response to which
        // we'll attach the terminal's Xaml control to the Xaml root.
        _tabView.SelectedItem(tabViewItem);
    }

    // Method Description:
    // - Create a new tab using a specified pane as the root.
    // Arguments:
    // - pane: The pane to use as the root.
    // - insertPosition: Optional parameter to indicate the position of tab.
    TerminalApp::Tab TerminalPage::_CreateNewTabFromPane(std::shared_ptr<Pane> pane, uint32_t insertPosition)
    {
        if (pane)
        {
            auto newTabImpl = winrt::make_self<Tab>(pane);
            _InitializeTab(newTabImpl, insertPosition);
            return *newTabImpl;
        }
        return nullptr;
    }

    // Method Description:
    // - Get the icon of the currently focused terminal control, and set its
    //   tab's icon to that icon.
    // Arguments:
    // - tab: the Tab to update the title for.
    void TerminalPage::_UpdateTabIcon(Tab& tab)
    {
        if (const auto content{ tab.GetActiveContent() })
        {
            const auto& icon{ content.Icon() };
            const auto theme = _settings.GlobalSettings().CurrentTheme();
            const auto iconStyle = (theme && theme.Tab()) ? theme.Tab().IconStyle() : IconStyle::Default;

            tab.UpdateIcon(icon, iconStyle);
        }
    }

    // Method Description:
    // - Handle changes to the tab width set by the user
    void TerminalPage::_UpdateTabWidthMode()
    {
        _tabView.TabWidthMode(_settings.GlobalSettings().TabWidthMode());
    }

    // Method Description:
    // - Handle changes in tab layout.
    void TerminalPage::_UpdateTabView()
    {
        // The tab row should only be visible if:
        // - we're not in focus mode
        // - we're not in full screen, or the user has enabled fullscreen tabs
        // - there is more than one tab, or the user has chosen to always show tabs
        const auto isVisible = !_isInFocusMode &&
                               (!_isFullscreen || _showTabsFullscreen) &&
                               (_settings.GlobalSettings().ShowTabsInTitlebar() ||
                                (_tabs.Size() > 1) ||
                                _settings.GlobalSettings().AlwaysShowTabs());

        if (_tabView)
        {
            // collapse/show the tabs themselves
            _tabView.Visibility(isVisible ? Visibility::Visible : Visibility::Collapsed);
        }
        if (_tabRow)
        {
            // collapse/show the row that the tabs are in.
            // NaN is the special value XAML uses for "Auto" sizing.
            _tabRow.Height(isVisible ? NAN : 0);
        }
    }

    // Method Description:
    // - Duplicates the current focused tab
    void TerminalPage::_DuplicateFocusedTab()
    {
        if (const auto activeTab{ _GetFocusedTabImpl() })
        {
            _DuplicateTab(*activeTab);
        }
    }

    // Method Description:
    // - Duplicates specified tab
    // Arguments:
    // - tab: tab to duplicate
    void TerminalPage::_DuplicateTab(const Tab& tab)
    {
        try
        {
            // TODO: GH#5047 - We're duplicating the whole profile, which might
            // be a dangling reference to old settings.
            //
            // In the future, it may be preferable to just duplicate the
            // current control's live settings (which will include changes
            // made through VT).
            uint32_t insertPosition = _tabs.Size();
            if (_settings.GlobalSettings().NewTabPosition() == NewTabPosition::AfterCurrentTab)
            {
                insertPosition = tab.TabViewIndex() + 1;
            }
            _CreateNewTabFromPane(_MakePane(nullptr, tab, nullptr), insertPosition);

            const auto runtimeTabText{ tab.GetTabText() };
            if (!runtimeTabText.empty())
            {
                if (auto newTab{ _GetFocusedTabImpl() })
                {
                    newTab->SetTabText(runtimeTabText);
                }
            }
        }
        CATCH_LOG();
    }

    // Method Description:
    // - Exports the content of the Terminal Buffer inside the tab
    // Arguments:
    // - tab: tab to export
    safe_void_coroutine TerminalPage::_ExportTab(const Tab& tab, winrt::hstring filepath)
    {
        // This will be used to set up the file picker "filter", to select .txt
        // files by default.
        static constexpr COMDLG_FILTERSPEC supportedFileTypes[] = {
            { L"Text Files (*.txt)", L"*.txt" },
            { L"All Files (*.*)", L"*.*" }
        };
        // An arbitrary GUID to associate with all instances of this
        // dialog, so they all re-open in the same path as they were
        // open before:
        static constexpr winrt::guid clientGuidExportFile{ 0xF6AF20BB, 0x0800, 0x48E6, { 0xB0, 0x17, 0xA1, 0x4C, 0xD8, 0x73, 0xDD, 0x58 } };

        try
        {
            if (const auto control{ tab.GetActiveTerminalControl() })
            {
                auto path = filepath;

                if (path.empty())
                {
                    // GH#11356 - we can't use the UWP apis for writing the file,
                    // because they don't work elevated (shocker) So just use the
                    // shell32 file picker manually.
                    std::wstring filename{ tab.Title() };
                    filename = til::clean_filename(filename);
                    path = co_await SaveFilePicker(*_hostingHwnd, [filename = std::move(filename)](auto&& dialog) {
                        THROW_IF_FAILED(dialog->SetClientGuid(clientGuidExportFile));
                        try
                        {
                            // Default to the Downloads folder
                            auto folderShellItem{ winrt::capture<IShellItem>(&SHGetKnownFolderItem, FOLDERID_Downloads, KF_FLAG_DEFAULT, nullptr) };
                            dialog->SetDefaultFolder(folderShellItem.get());
                        }
                        CATCH_LOG(); // non-fatal
                        THROW_IF_FAILED(dialog->SetFileTypes(ARRAYSIZE(supportedFileTypes), supportedFileTypes));
                        THROW_IF_FAILED(dialog->SetFileTypeIndex(1)); // the array is 1-indexed
                        THROW_IF_FAILED(dialog->SetDefaultExtension(L"txt"));

                        // Default to using the tab title as the file name
                        THROW_IF_FAILED(dialog->SetFileName((filename + L".txt").c_str()));
                    });
                }
                else
                {
                    // The file picker isn't going to give us paths with
                    // environment variables, but the user might have set one in
                    // the settings. Expand those here.

                    path = winrt::hstring{ wil::ExpandEnvironmentStringsW<std::wstring>(path.c_str()) };
                }

                if (!path.empty())
                {
                    const auto buffer = control.ReadEntireBuffer();
                    til::io::write_utf8_string_to_file_atomic(std::filesystem::path{ std::wstring_view{ path } }, til::u16u8(buffer));
                }
            }
        }
        CATCH_LOG();
    }

    // Method Description:
    // - Record the configuration information of the last closed thing .
    // - Will occasionally prune the list so it doesn't grow infinitely.
    // Arguments:
    // - args: the list of actions to take to remake the pane/tab
    void TerminalPage::_AddPreviouslyClosedPaneOrTab(std::vector<ActionAndArgs>&& args)
    {
        // Just make sure we don't get infinitely large, but still
        // maintain a large replay buffer.
        if (const auto size = _previouslyClosedPanesAndTabs.size(); size > 150)
        {
            const auto it = _previouslyClosedPanesAndTabs.begin();
            // delete 50 at a time so that we don't have to do an erase
            // of the buffer every time when at capacity.
            _previouslyClosedPanesAndTabs.erase(it, it + (size - 100));
        }

        _previouslyClosedPanesAndTabs.emplace_back(args);
    }

    // Method Description:
    // - Removes the tab (both TerminalControl and XAML) after prompting for approval
    // Arguments:
    // - tab: the tab to remove
    winrt::Windows::Foundation::IAsyncAction TerminalPage::_HandleCloseTabRequested(winrt::TerminalApp::Tab tab)
    {
        winrt::com_ptr<TerminalPage> strong;

        if (tab.ReadOnly())
        {
            const auto weak = get_weak();

            auto warningResult = co_await _ShowCloseReadOnlyDialog();

            strong = weak.get();

            // If the user didn't explicitly click on close tab - leave
            if (!strong || warningResult != ContentDialogResult::Primary)
            {
                co_return;
            }
        }

        auto t = winrt::get_self<implementation::Tab>(tab);
        auto actions = t->BuildStartupActions(BuildStartupKind::None);
        _AddPreviouslyClosedPaneOrTab(std::move(actions));

        tab.Close();
    }

    // Removes the tab (both TerminalControl and XAML).
    // NOTE: Don't call this directly, but rather `tab.Close()`.
    void TerminalPage::_RemoveTab(const winrt::TerminalApp::Tab& tab)
    {
        uint32_t tabIndex{};
        if (!_tabs.IndexOf(tab, tabIndex))
        {
            // The tab is already removed
            return;
        }

        // We use _removing flag to suppress _OnTabSelectionChanged events
        // that might get triggered while removing
        _removing = true;
        auto unsetRemoving = wil::scope_exit([&]() noexcept { _removing = false; });

        const auto focusedTabIndex{ _GetFocusedTabIndex() };

        // Removing the tab from the collection should destroy its control and disconnect its connection,
        // but it doesn't always do so. The UI tree may still be holding the control and preventing its destruction.
        tab.Shutdown();

        uint32_t mruIndex{};
        if (_mruTabs.IndexOf(tab, mruIndex))
        {
            _mruTabs.RemoveAt(mruIndex);
        }

        if (tab == _settingsTab)
        {
            _settingsTab = nullptr;
        }

        if (_stashed.draggedTab && *_stashed.draggedTab == tab)
        {
            _stashed.draggedTab = nullptr;
        }

        _tabs.RemoveAt(tabIndex);
        _tabView.TabItems().RemoveAt(tabIndex);
        _UpdateTabIndices();

        // To close the window here, we need to close the hosting window.
        if (_tabs.Size() == 0)
        {
            // If we are supposed to save state, make sure we clear it out
            // if the user manually closed all tabs.
            // Do this only if we are the last window; the monarch will notice
            // we are missing and remove us that way otherwise.
            CloseWindowRequested.raise(*this, nullptr);
        }
        else if (focusedTabIndex.has_value() && focusedTabIndex.value() == gsl::narrow_cast<uint32_t>(tabIndex))
        {
            // Manually select the new tab to get focus, rather than relying on TabView since:
            // 1. We want to customize this behavior (e.g., use MRU logic)
            // 2. In fullscreen (GH#5799) and focus (GH#7916) modes the _OnTabItemsChanged is not fired
            // 3. When rearranging tabs (GH#7916) _OnTabItemsChanged is suppressed
            const auto tabSwitchMode = _settings.GlobalSettings().TabSwitcherMode();

            if (tabSwitchMode == TabSwitcherMode::MostRecentlyUsed)
            {
                const auto newSelectedTab = _mruTabs.GetAt(0);
                _UpdatedSelectedTab(newSelectedTab);
                _tabView.SelectedItem(newSelectedTab.TabViewItem());
            }
            else
            {
                // We can't use
                //   auto selectedIndex = _tabView.SelectedIndex();
                // Because this will always return -1 in this scenario unfortunately.
                //
                // So, what we're going to try to do is move the focus to the tab
                // to the right, within the bounds of how many tabs we have.
                //
                // EX: we have 4 tabs: [A, B, C, D]. If we close:
                // * A (tabIndex=0): We'll want to focus tab B (now in index 0)
                // * B (tabIndex=1): We'll want to focus tab C (now in index 1)
                // * C (tabIndex=2): We'll want to focus tab D (now in index 2)
                // * D (tabIndex=3): We'll want to focus tab C (now in index 2)
                const auto newSelectedIndex = std::clamp<int32_t>(tabIndex, 0, _tabs.Size() - 1);
                // _UpdatedSelectedTab will do the work of setting up the new tab as
                // the focused one, and unfocusing all the others.
                auto newSelectedTab{ _tabs.GetAt(newSelectedIndex) };
                _UpdatedSelectedTab(newSelectedTab);

                // Also, we need to _manually_ set the SelectedItem of the tabView
                // here. If we don't, then the TabView will technically not have a
                // selected item at all, which can make things like ClosePane not
                // work correctly.
                _tabView.SelectedItem(newSelectedTab.TabViewItem());
            }
        }

        // GH#5559 - If we were in the middle of a drag/drop, end it by clearing
        // out our state.
        if (_rearranging)
        {
            _rearranging = false;
            _rearrangeFrom = std::nullopt;
            _rearrangeTo = std::nullopt;
        }
    }

    // Method Description:
    // - Sets focus to the tab to the right or left the currently selected tab.
    void TerminalPage::_SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference<Microsoft::Terminal::Settings::Model::TabSwitcherMode>& customTabSwitcherMode)
    {
        const auto index{ _GetFocusedTabIndex().value_or(0) };
        const auto tabSwitchMode = customTabSwitcherMode ? customTabSwitcherMode.Value() : _settings.GlobalSettings().TabSwitcherMode();
        if (tabSwitchMode == TabSwitcherMode::Disabled)
        {
            auto tabCount = _tabs.Size();
            // Wraparound math. By adding tabCount and then calculating
            // modulo tabCount, we clamp the values to the range [0,
            // tabCount) while still supporting moving leftward from 0 to
            // tabCount - 1.
            const auto newTabIndex = ((tabCount + index + (bMoveRight ? 1 : -1)) % tabCount);
            _SelectTab(newTabIndex);
        }
        else
        {
            const auto p = LoadCommandPalette();
            p.SetTabs(_tabs, _mruTabs);

            // Otherwise, set up the tab switcher in the selected mode, with
            // the given ordering, and make it visible.
            p.EnableTabSwitcherMode(index, tabSwitchMode);
            p.Visibility(Visibility::Visible);
            p.SelectNextItem(bMoveRight);
        }
    }

    // Method Description:
    // - Sets focus to the desired tab. Returns false if the provided tabIndex
    //   is greater than the number of tabs we have.
    // - During startup, we'll immediately set the selected tab as focused.
    // - After startup, we'll dispatch an async method to set the selected
    //   item of the TabView, which will then also trigger a
    //   TabView::SelectionChanged, handled in
    //   TerminalPage::_OnTabSelectionChanged
    // Return Value:
    // true iff we were able to select that tab index, false otherwise
    bool TerminalPage::_SelectTab(uint32_t tabIndex)
    {
        // GH#9369 - if the argument is out of range, then clamp to the number
        // of available tabs. Previously, we'd just silently do nothing if the
        // value was greater than the number of tabs.
        tabIndex = std::clamp(tabIndex, 0u, _tabs.Size() - 1);

        auto tab{ _tabs.GetAt(tabIndex) };
        // GH#11107 - Always just set the item directly first so that if
        // tab movement is done as part of multiple actions following calls
        // to _GetFocusedTab will return the correct tab.
        _tabView.SelectedItem(tab.TabViewItem());

        if (_startupState == StartupState::InStartup)
        {
            _UpdatedSelectedTab(tab);
        }
        else
        {
            _SetFocusedTab(tab);
        }

        return true;
    }

    // Method Description:
    // - This method is called once a tab was selected in tab switcher
    //   We'll use this event to select the relevant tab
    // Arguments:
    // - tab - tab to select
    // Return Value:
    // - <none>
    void TerminalPage::_OnSwitchToTabRequested(const IInspectable& /*sender*/, const winrt::TerminalApp::Tab& tab)
    {
        uint32_t index{};
        if (_tabs.IndexOf(tab, index))
        {
            _SelectTab(index);
        }
    }

    // Method Description:
    // - Returns the index in our list of tabs of the currently focused tab. If
    //      no tab is currently selected, returns nullopt.
    // Return Value:
    // - the index of the currently focused tab if there is one, else nullopt
    std::optional<uint32_t> TerminalPage::_GetFocusedTabIndex() const noexcept
    {
        // GH#1117: This is a workaround because _tabView.SelectedIndex()
        //          sometimes return incorrect result after removing some tabs
        uint32_t focusedIndex;
        if (_tabView.TabItems().IndexOf(_tabView.SelectedItem(), focusedIndex))
        {
            return focusedIndex;
        }
        return std::nullopt;
    }

    // Method Description:
    // - Returns the index in our list of tabs of the currently focused tab. If
    //      no tab is currently selected, returns nullopt.
    // Return Value:
    // - the index of the currently focused tab if there is one, else nullopt
    std::optional<uint32_t> TerminalPage::_GetTabIndex(const TerminalApp::Tab& tab) const noexcept
    {
        uint32_t i;
        if (_tabs.IndexOf(tab, i))
        {
            return i;
        }
        return std::nullopt;
    }

    // Method Description:
    // - returns the currently focused tab. This might return null,
    //   so make sure to check the result!
    winrt::TerminalApp::Tab TerminalPage::_GetFocusedTab() const noexcept
    {
        if (auto index{ _GetFocusedTabIndex() })
        {
            return _tabs.GetAt(*index);
        }
        return nullptr;
    }

    // Method Description:
    // - returns a com_ptr to the currently focused tab implementation. This might return null,
    //   so make sure to check the result!
    winrt::com_ptr<Tab> TerminalPage::_GetFocusedTabImpl() const noexcept
    {
        if (auto tab{ _GetFocusedTab() })
        {
            return _GetTabImpl(tab);
        }
        return nullptr;
    }

    // Method Description:
    // - returns a tab corresponding to a view item. This might return null,
    //   so make sure to check the result!
    winrt::TerminalApp::Tab TerminalPage::_GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept
    {
        uint32_t tabIndexFromControl{};
        const auto items{ _tabView.TabItems() };
        if (items.IndexOf(tabViewItem, tabIndexFromControl) && tabIndexFromControl < _tabs.Size())
        {
            // If IndexOf returns true, we've actually got an index
            return _tabs.GetAt(tabIndexFromControl);
        }
        return nullptr;
    }

    // Method Description:
    // - An async method for changing the focused tab on the UI thread. This
    //   method will _only_ set the selected item of the TabView, which will
    //   then also trigger a TabView::SelectionChanged event, which we'll handle
    //   in TerminalPage::_OnTabSelectionChanged, where we'll mark the new tab
    //   as focused.
    // Arguments:
    // - tab: tab to focus.
    // Return Value:
    // - <none>
    safe_void_coroutine TerminalPage::_SetFocusedTab(const winrt::TerminalApp::Tab tab)
    {
        // GH#1117: This is a workaround because _tabView.SelectedIndex(tabIndex)
        //          sometimes set focus to an incorrect tab after removing some tabs
        auto weakThis{ get_weak() };

        if (!_tabView.Dispatcher().HasThreadAccess())
        {
            co_await winrt::resume_foreground(_tabView.Dispatcher());
        }

        if (auto page{ weakThis.get() })
        {
            // Make sure the tab was not removed
            uint32_t tabIndex{};
            if (_tabs.IndexOf(tab, tabIndex))
            {
                _tabView.SelectedItem(tab.TabViewItem());
            }
        }
    }

    // Method Description:
    // - Disables read-only mode on pane if the user wishes to close it and read-only mode is enabled.
    // Arguments:
    // - pane: the pane that is about to be closed.
    // Return Value:
    // - bool indicating whether the (read-only) pane can be closed.
    winrt::Windows::Foundation::IAsyncOperation<bool> TerminalPage::_PaneConfirmCloseReadOnly(std::shared_ptr<Pane> pane)
    {
        if (pane->ContainsReadOnly())
        {
            const auto weak = get_weak();

            auto warningResult = co_await _ShowCloseReadOnlyDialog();

            const auto strong = weak.get();

            // If the user didn't explicitly click on close tab - leave
            if (!strong || warningResult != ContentDialogResult::Primary)
            {
                co_return false;
            }

            // Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab
            pane->WalkTree([](const auto& p) {
                if (const auto control{ p->GetTerminalControl() })
                {
                    if (control.ReadOnly())
                    {
                        control.ToggleReadOnly();
                    }
                }
            });
        }
        co_return true;
    }

    // Method Description:
    // - Removes the pane from the tab it belongs to.
    // Arguments:
    // - pane: the pane to close.
    void TerminalPage::_HandleClosePaneRequested(std::shared_ptr<Pane> pane)
    {
        // Build the list of actions to recreate the closed pane,
        // BuildStartupActions returns the "first" pane and the rest of
        // its actions are assuming that first pane has been created first.
        // This doesn't handle refocusing anything in particular, the
        // result will be that the last pane created is focused. In the
        // case of a single pane that is the desired behavior anyways.
        auto state = pane->BuildStartupActions(0, 1, BuildStartupKind::None);
        {
            ActionAndArgs splitPaneAction{};
            splitPaneAction.Action(ShortcutAction::SplitPane);
            SplitPaneArgs splitPaneArgs{ SplitDirection::Automatic, state.firstPane->GetTerminalArgsForPane(BuildStartupKind::None) };
            splitPaneAction.Args(splitPaneArgs);

            state.args.emplace(state.args.begin(), std::move(splitPaneAction));
        }
        _AddPreviouslyClosedPaneOrTab(std::move(state.args));

        // If specified, detach before closing to directly update the pane structure
        pane->Close();
    }

    // Method Description:
    // - Close the currently focused pane. If the pane is the last pane in the
    //   tab, the tab will also be closed. This will happen when we handle the
    //   tab's Closed event.
    safe_void_coroutine TerminalPage::_CloseFocusedPane()
    {
        if (const auto activeTab{ _GetFocusedTabImpl() })
        {
            _UnZoomIfNeeded();

            if (const auto pane{ activeTab->GetActivePane() })
            {
                const auto weak = get_weak();
                if (co_await _PaneConfirmCloseReadOnly(pane))
                {
                    if (const auto strong = weak.get())
                    {
                        _HandleClosePaneRequested(pane);
                    }
                }
            }
        }
    }

    // Method Description:
    // - Close all panes with the given IDs sequentially.
    // Arguments:
    // - weakTab: weak reference to the tab that the pane belongs to.
    // - paneIds: collection of the IDs of the panes that are marked for removal.
    void TerminalPage::_ClosePanes(weak_ref<Tab> weakTab, std::vector<uint32_t> paneIds)
    {
        if (auto strongTab{ weakTab.get() })
        {
            // Close all unfocused panes one by one
            while (!paneIds.empty())
            {
                const auto id = paneIds.back();
                paneIds.pop_back();

                if (const auto pane{ strongTab->GetRootPane()->FindPane(id) })
                {
                    pane->ClosedByParent([ids{ std::move(paneIds) }, weakThis{ get_weak() }, weakTab]() {
                        if (auto strongThis{ weakThis.get() })
                        {
                            strongThis->_ClosePanes(weakTab, std::move(ids));
                        }
                    });

                    // Close the pane which will eventually trigger the closed by parent event
                    _HandleClosePaneRequested(pane);
                    break;
                }
            }
        }
    }

    // Method Description:
    // - Close the tab at the given index.
    void TerminalPage::_CloseTabAtIndex(uint32_t index)
    {
        if (index >= _tabs.Size())
        {
            return;
        }
        if (auto tab{ _tabs.GetAt(index) })
        {
            _HandleCloseTabRequested(tab);
        }
    }

    // Method Description:
    // - Closes provided tabs one by one
    // Arguments:
    // - tabs - tabs to remove
    safe_void_coroutine TerminalPage::_RemoveTabs(const std::vector<winrt::TerminalApp::Tab> tabs)
    {
        const auto weak = get_weak();

        for (auto& tab : tabs)
        {
            winrt::Windows::Foundation::IAsyncAction action{ nullptr };
            if (const auto strong = weak.get())
            {
                action = _HandleCloseTabRequested(tab);
            }

            if (!action)
            {
                co_return;
            }

            co_await action;
        }
    }
    // Method Description:
    // - Responds to changes in the TabView's item list by changing the
    //   tabview's visibility.
    // - This method is also invoked when tabs are dragged / dropped as part of
    //   tab reordering and this method hands that case as well in concert with
    //   TabDragStarting and TabDragCompleted handlers that are set up in
    //   TerminalPage::Create()
    // Arguments:
    // - sender: the control that originated this event
    // - eventArgs: the event's constituent arguments
    void TerminalPage::_OnTabItemsChanged(const IInspectable& /*sender*/, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs)
    {
        if (_rearranging)
        {
            if (eventArgs.CollectionChange() == Windows::Foundation::Collections::CollectionChange::ItemRemoved)
            {
                _rearrangeFrom = eventArgs.Index();
            }

            if (eventArgs.CollectionChange() == Windows::Foundation::Collections::CollectionChange::ItemInserted)
            {
                _rearrangeTo = eventArgs.Index();
            }
        }

        if (const auto p = CommandPaletteElement())
        {
            p.Visibility(Visibility::Collapsed);
        }
        _UpdateTabView();
    }

    void TerminalPage::_OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e)
    {
        if (!_tabItemMiddleClickHookEnabled || !e.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
        {
            return;
        }

        const auto tabViewItem = sender.try_as<MUX::Controls::TabViewItem>();
        if (!tabViewItem || !tabViewItem.CapturePointer(e.Pointer()))
        {
            return;
        }

        _tabItemMiddleClickExited = false;

        _tabItemMiddleClickPointerEntered = tabViewItem.PointerEntered(winrt::auto_revoke, [this](auto&&, auto&& e) {
            _tabItemMiddleClickExited = false;
            e.Handled(true);
        });
        _tabItemMiddleClickPointerExited = tabViewItem.PointerExited(winrt::auto_revoke, [this](auto&&, auto&& e) {
            _tabItemMiddleClickExited = true;
            e.Handled(true);
        });
        _tabItemMiddleClickPointerCaptureLost = tabViewItem.PointerCaptureLost(winrt::auto_revoke, [this](auto&& sender, auto&& e) {
            // The WinUI TabView calls CapturePointer() internally and it's not reference counted,
            // so when it calls ReleasePointerCapture() in its PointerReleased handler,
            // we get a PointerCaptureLost before we receive the PointerReleased event.
            // This makes typical handling of PointerReleased events on our side difficult.
            // Well, whatever, now we just hook PointerCaptureLost because we know WinUI will trigger it.

            _tabItemMiddleClickPointerEntered.revoke();
            _tabItemMiddleClickPointerExited.revoke();
            _tabItemMiddleClickPointerCaptureLost.revoke();

            if (!_tabItemMiddleClickExited && !e.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
            {
                _OnTabPointerReleasedCloseTab(std::move(sender));
            }

            e.Handled(true);
        });
        e.Handled(true);
    }

    safe_void_coroutine TerminalPage::_OnTabPointerReleasedCloseTab(IInspectable sender)
    {
        // WinUI asynchronously updates its tab view items, so it may happen that we're given a
        // `TabViewItem` that still contains a `Tab` which has actually already been removed.
        // First we must yield once, to flush out whatever TabView is currently doing.
        const auto weak = get_weak();
        co_await wil::resume_foreground(Dispatcher());
        const auto strong = weak.get();
        if (!strong)
        {
            co_return;
        }

        const auto tab = _GetTabByTabViewItem(sender);
        if (!tab)
        {
            co_return;
        }

        // `tab.Shutdown()` in `_RemoveTab()` sets the content to null = This checks if the tab is closed.
        if (tab.Content())
        {
            _HandleCloseTabRequested(tab);
        }
    }

    void TerminalPage::_UpdatedSelectedTab(const winrt::TerminalApp::Tab& tab)
    {
        // Unfocus all the tabs.
        for (const auto& tab : _tabs)
        {
            tab.Focus(FocusState::Unfocused);
        }

        try
        {
            _tabContent.Children().Clear();
            _tabContent.Children().Append(tab.Content());

            // GH#7409: If the tab switcher is open, then we _don't_ want to
            // automatically focus the new tab here. The tab switcher wants
            // to be able to "preview" the selected tab as the user tabs
            // through the menu, but if we toss the focus to the control
            // here, then the user won't be able to navigate the ATS any
            // longer.
            //
            // When the tab switcher is eventually dismissed, the focus will
            // get tossed back to the focused terminal control, so we don't
            // need to worry about focus getting lost.
            const auto p = CommandPaletteElement();
            if (!p || p.Visibility() != Visibility::Visible)
            {
                tab.Focus(FocusState::Programmatic);
                _UpdateMRUTab(tab);
                _updateAllTabCloseButtons();
            }

            tab.TabViewItem().StartBringIntoView();

            // Raise an event that our title changed
            TitleChanged.raise(*this, nullptr);

            _updateThemeColors();

            auto tabImpl = _GetTabImpl(tab);
            if (tabImpl)
            {
                auto profile = tabImpl->GetFocusedProfile();
                _UpdateBackground(profile);
            }

            _adjustProcessPriorityThrottled->Run();
        }
        CATCH_LOG();
    }

    void TerminalPage::_UpdateBackground(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile)
    {
        if (profile && _settings.GlobalSettings().UseBackgroundImageForWindow())
        {
            _SetBackgroundImage(profile.DefaultAppearance());
        }
    }

    // Method Description:
    // - Responds to the TabView control's Selection Changed event (to move a
    //      new terminal control into focus) when not in in the middle of a tab rearrangement.
    // Arguments:
    // - sender: the control that originated this event
    // - eventArgs: the event's constituent arguments
    void TerminalPage::_OnTabSelectionChanged(const IInspectable& sender, const WUX::Controls::SelectionChangedEventArgs& /*eventArgs*/)
    {
        if (!_rearranging && !_removing)
        {
            auto tabView = sender.as<MUX::Controls::TabView>();
            auto selectedIndex = tabView.SelectedIndex();
            if (selectedIndex >= 0 && selectedIndex < gsl::narrow_cast<int32_t>(_tabs.Size()))
            {
                const auto tab{ _tabs.GetAt(selectedIndex) };
                _UpdatedSelectedTab(tab);
            }
        }
    }

    // Method Description:
    // - Updates all tabs with their current index in _tabs.
    // Arguments:
    // - <none>
    // Return Value:
    // - <none>
    void TerminalPage::_UpdateTabIndices()
    {
        const auto size = _tabs.Size();
        for (uint32_t i = 0; i < size; ++i)
        {
            auto tab{ _tabs.GetAt(i) };
            auto tabImpl{ winrt::get_self<Tab>(tab) };
            tabImpl->UpdateTabViewIndex(i, size);
        }
    }

    // Method Description:
    // - Bumps the tab in its in-order index up to the top of the mru list.
    // Arguments:
    // - tab: tab to bump.
    // Return Value:
    // - <none>
    void TerminalPage::_UpdateMRUTab(const winrt::TerminalApp::Tab& tab)
    {
        uint32_t mruIndex;
        if (_mruTabs.IndexOf(tab, mruIndex))
        {
            if (mruIndex > 0)
            {
                _mruTabs.RemoveAt(mruIndex);
                _mruTabs.InsertAt(0, tab);
            }
        }
    }

    // Method Description:
    // - Moves the tab to another index in the tabs row (if required).
    // Arguments:
    // - currentTabIndex: the current index of the tab to move
    // - suggestedNewTabIndex: the new index of the tab, might get clamped to fit int the tabs row boundaries
    // Return Value:
    // - <none>
    void TerminalPage::_TryMoveTab(const uint32_t currentTabIndex,
                                   const int32_t suggestedNewTabIndex)
    {
        auto newTabIndex = gsl::narrow_cast<uint32_t>(std::clamp<int32_t>(suggestedNewTabIndex, 0, _tabs.Size() - 1));
        if (currentTabIndex != newTabIndex)
        {
            auto tab = _tabs.GetAt(currentTabIndex);
            auto tabViewItem = tab.TabViewItem();
            _tabs.RemoveAt(currentTabIndex);
            _tabs.InsertAt(newTabIndex, tab);
            _UpdateTabIndices();

            _tabView.TabItems().RemoveAt(currentTabIndex);
            _tabView.TabItems().InsertAt(newTabIndex, tabViewItem);
            _tabView.SelectedItem(tabViewItem);

            if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this))
            {
                const auto tabTitle = tab.Title();
                autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted,
                                                Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent,
                                                RS_fmt(L"TerminalPage_TabMovedAnnouncement_Direction", tabTitle, newTabIndex + 1),
                                                L"TerminalPageMoveTabWithDirection" /* unique name for this notification category */);
            }
        }
    }

    void TerminalPage::_TabDragStarted(const IInspectable& /*sender*/,
                                       const IInspectable& /*eventArgs*/)
    {
        _rearranging = true;
        _rearrangeFrom = std::nullopt;
        _rearrangeTo = std::nullopt;
    }

    void TerminalPage::_TabDragCompleted(const IInspectable& /*sender*/,
                                         const IInspectable& /*eventArgs*/)
    {
        auto& from{ _rearrangeFrom };
        auto& to{ _rearrangeTo };

        if (from.has_value() && to.has_value() && to != from)
        {
            try
            {
                auto& tabs{ _tabs };
                auto tab = tabs.GetAt(from.value());
                tabs.RemoveAt(from.value());
                tabs.InsertAt(to.value(), tab);
                _UpdateTabIndices();
            }
            CATCH_LOG();
        }

        _rearranging = false;

        if (to.has_value() &&
            *to < gsl::narrow_cast<int32_t>(TabRow().TabView().TabItems().Size()))
        {
            // Selecting the dropped tab
            TabRow().TabView().SelectedIndex(to.value());
        }

        from = std::nullopt;
        to = std::nullopt;
    }

    void TerminalPage::_DismissTabContextMenus()
    {
        for (const auto& tab : _tabs)
        {
            if (tab.TabViewItem().ContextFlyout())
            {
                tab.TabViewItem().ContextFlyout().Hide();
            }
        }
    }

    void TerminalPage::_FocusCurrentTab(const bool focusAlways)
    {
        // We don't want to set focus on the tab if fly-out is open as it will
        // be closed TODO GH#5400: consider checking we are not in the opening
        // state, by hooking both Opening and Open events
        if (focusAlways || !_newTabButton.Flyout().IsOpen())
        {
            // Return focus to the active control
            if (auto tab{ _GetFocusedTab() })
            {
                tab.Focus(FocusState::Programmatic);
                _UpdateMRUTab(tab);
                _updateAllTabCloseButtons();
            }
        }
    }

    bool TerminalPage::_HasMultipleTabs() const
    {
        return _tabs.Size() > 1;
    }
}
