// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "pch.h"
#include "MainPage.h"
#include "MainPage.g.cpp"
#include "Launch.h"
#include "Interaction.h"
#include "Rendering.h"
#include "Actions.h"
#include "ReadOnlyActions.h"
#include "Profiles.h"
#include "GlobalAppearance.h"
#include "ColorSchemes.h"
#include "AddProfile.h"
#include "..\types\inc\utils.hpp"

#include <LibraryResources.h>

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

using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::System;
using namespace winrt::Windows::UI::Xaml::Controls;

static const std::wstring_view launchTag{ L"Launch_Nav" };
static const std::wstring_view interactionTag{ L"Interaction_Nav" };
static const std::wstring_view renderingTag{ L"Rendering_Nav" };
static const std::wstring_view actionsTag{ L"Actions_Nav" };
static const std::wstring_view globalProfileTag{ L"GlobalProfile_Nav" };
static const std::wstring_view addProfileTag{ L"AddProfile" };
static const std::wstring_view colorSchemesTag{ L"ColorSchemes_Nav" };
static const std::wstring_view globalAppearanceTag{ L"GlobalAppearance_Nav" };

namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
    static Editor::ProfileViewModel _viewModelForProfile(const Model::Profile& profile, const Model::CascadiaSettings& appSettings)
    {
        return winrt::make<implementation::ProfileViewModel>(profile, appSettings);
    }

    MainPage::MainPage(const CascadiaSettings& settings) :
        _settingsSource{ settings },
        _settingsClone{ settings.Copy() }
    {
        InitializeComponent();

        _InitializeProfilesList();

        _colorSchemesNavState = winrt::make<ColorSchemesPageNavigationState>(_settingsClone);

        Automation::AutomationProperties::SetHelpText(SaveButton(), RS_(L"Settings_SaveSettingsButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
        Automation::AutomationProperties::SetHelpText(ResetButton(), RS_(L"Settings_ResetSettingsButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
        Automation::AutomationProperties::SetHelpText(OpenJsonNavItem(), RS_(L"Nav_OpenJSON/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
    }

    // Method Description:
    // - Update the Settings UI with a new CascadiaSettings to bind to
    // Arguments:
    // - settings - the new settings source
    // Return value:
    // - <none>
    void MainPage::UpdateSettings(const Model::CascadiaSettings& settings)
    {
        _settingsSource = settings;
        _settingsClone = settings.Copy();

        // Deduce information about the currently selected item
        IInspectable selectedItemTag;
        auto menuItems{ SettingsNav().MenuItems() };
        if (const auto& selectedItem{ SettingsNav().SelectedItem() })
        {
            if (const auto& navViewItem{ selectedItem.try_as<MUX::Controls::NavigationViewItem>() })
            {
                selectedItemTag = navViewItem.Tag();
            }
        }

        // We'll remove a bunch of items and iterate over it twice.
        // --> Copy it into an STL vector to simplify our code and reduce COM overhead.
        std::vector<IInspectable> menuItemsSTL(menuItems.Size(), nullptr);
        menuItems.GetMany(0, menuItemsSTL);

        // We want to refresh the list of profiles in the NavigationView.
        // In order to add profiles we can use _InitializeProfilesList();
        // But before we can do that we have to remove existing profiles first of course.
        // This "erase-remove" idiom will achieve just that.
        menuItemsSTL.erase(
            std::remove_if(
                menuItemsSTL.begin(),
                menuItemsSTL.end(),
                [](const auto& item) -> bool {
                    if (const auto& navViewItem{ item.try_as<MUX::Controls::NavigationViewItem>() })
                    {
                        if (const auto& tag{ navViewItem.Tag() })
                        {
                            if (tag.try_as<Editor::ProfileViewModel>())
                            {
                                // remove NavViewItem pointing to a Profile
                                return true;
                            }
                            if (const auto& stringTag{ tag.try_as<hstring>() })
                            {
                                if (stringTag == addProfileTag)
                                {
                                    // remove the "Add Profile" item
                                    return true;
                                }
                            }
                        }
                    }
                    return false;
                }),
            menuItemsSTL.end());

        menuItems.ReplaceAll(menuItemsSTL);

        // Repopulate profile-related menu items
        _InitializeProfilesList();
        // Update the Nav State with the new version of the settings
        _colorSchemesNavState.Settings(_settingsClone);
        // We'll update the profile in the _profilesNavState whenever we actually navigate to one

        // now that the menuItems are repopulated,
        // refresh the current page using the SelectedItem data we collected before the refresh
        if (selectedItemTag)
        {
            for (const auto& item : menuItems)
            {
                if (const auto& menuItem{ item.try_as<MUX::Controls::NavigationViewItem>() })
                {
                    if (const auto& tag{ menuItem.Tag() })
                    {
                        if (const auto& stringTag{ tag.try_as<hstring>() })
                        {
                            if (const auto& selectedItemStringTag{ selectedItemTag.try_as<hstring>() })
                            {
                                if (stringTag == selectedItemStringTag)
                                {
                                    // found the one that was selected before the refresh
                                    SettingsNav().SelectedItem(item);
                                    _Navigate(*stringTag);
                                    return;
                                }
                            }
                        }
                        else if (const auto& profileTag{ tag.try_as<ProfileViewModel>() })
                        {
                            if (const auto& selectedItemProfileTag{ selectedItemTag.try_as<ProfileViewModel>() })
                            {
                                if (profileTag->OriginalProfileGuid() == selectedItemProfileTag->OriginalProfileGuid())
                                {
                                    // found the one that was selected before the refresh
                                    SettingsNav().SelectedItem(item);
                                    _Navigate(*profileTag);
                                    return;
                                }
                            }
                        }
                    }
                }
            }
        }

        // Couldn't find the selected item, fallback to first menu item
        // This happens when the selected item was a profile which doesn't exist in the new configuration
        // We can use menuItemsSTL here because the only things they miss are profile entries.
        const auto& firstItem{ menuItemsSTL.at(0).as<MUX::Controls::NavigationViewItem>() };
        SettingsNav().SelectedItem(firstItem);
        _Navigate(unbox_value<hstring>(firstItem.Tag()));
    }

    void MainPage::SetHostingWindow(uint64_t hostingWindow) noexcept
    {
        _hostingHwnd.emplace(reinterpret_cast<HWND>(hostingWindow));
    }

    bool MainPage::TryPropagateHostingWindow(IInspectable object) noexcept
    {
        if (_hostingHwnd)
        {
            if (auto initializeWithWindow{ object.try_as<IInitializeWithWindow>() })
            {
                return SUCCEEDED(initializeWithWindow->Initialize(*_hostingHwnd));
            }
        }
        return false;
    }

    // Method Description:
    // - Creates a new profile and navigates to it in the Settings UI
    // Arguments:
    // - profileGuid: the guid of the profile we want to duplicate,
    //                can be empty to indicate that we should create a fresh profile
    void MainPage::_AddProfileHandler(winrt::guid profileGuid)
    {
        uint32_t insertIndex;
        auto selectedItem{ SettingsNav().SelectedItem() };
        auto menuItems{ SettingsNav().MenuItems() };
        menuItems.IndexOf(selectedItem, insertIndex);
        if (profileGuid != winrt::guid{})
        {
            // if we were given a non-empty guid, we want to duplicate the corresponding profile
            const auto profile = _settingsClone.FindProfile(profileGuid);
            if (profile)
            {
                const auto duplicated = _settingsClone.DuplicateProfile(profile);
                _CreateAndNavigateToNewProfile(insertIndex, duplicated);
            }
        }
        else
        {
            // we were given an empty guid, create a new profile
            _CreateAndNavigateToNewProfile(insertIndex, nullptr);
        }
    }

    uint64_t MainPage::GetHostingWindow() const noexcept
    {
        return reinterpret_cast<uint64_t>(_hostingHwnd.value_or(nullptr));
    }

    // Function Description:
    // - Called when the NavigationView is loaded. Navigates to the first item in the NavigationView, if no item is selected
    // Arguments:
    // - <unused>
    // Return Value:
    // - <none>
    void MainPage::SettingsNav_Loaded(IInspectable const&, RoutedEventArgs const&)
    {
        if (SettingsNav().SelectedItem() == nullptr)
        {
            const auto initialItem = SettingsNav().MenuItems().GetAt(0);
            SettingsNav().SelectedItem(initialItem);

            // Manually navigate because setting the selected item programmatically doesn't trigger ItemInvoked.
            if (const auto tag = initialItem.as<MUX::Controls::NavigationViewItem>().Tag())
            {
                _Navigate(unbox_value<hstring>(tag));
            }
        }
    }

    // Function Description:
    // - Called when NavigationView items are invoked. Navigates to the corresponding page.
    // Arguments:
    // - args - additional event info from invoking the NavViewItem
    // Return Value:
    // - <none>
    void MainPage::SettingsNav_ItemInvoked(MUX::Controls::NavigationView const&, MUX::Controls::NavigationViewItemInvokedEventArgs const& args)
    {
        if (const auto clickedItemContainer = args.InvokedItemContainer())
        {
            if (clickedItemContainer.IsSelected())
            {
                // Clicked on the selected item.
                // Don't navigate to the same page again.
                return;
            }

            if (const auto navString = clickedItemContainer.Tag().try_as<hstring>())
            {
                _Navigate(*navString);
            }
            else if (const auto profile = clickedItemContainer.Tag().try_as<Editor::ProfileViewModel>())
            {
                // Navigate to a page with the given profile
                _Navigate(profile);
            }
        }
    }

    void MainPage::_Navigate(hstring clickedItemTag)
    {
        if (clickedItemTag == launchTag)
        {
            contentFrame().Navigate(xaml_typename<Editor::Launch>(), winrt::make<LaunchPageNavigationState>(_settingsClone));
        }
        else if (clickedItemTag == interactionTag)
        {
            contentFrame().Navigate(xaml_typename<Editor::Interaction>(), winrt::make<InteractionPageNavigationState>(_settingsClone.GlobalSettings()));
        }
        else if (clickedItemTag == renderingTag)
        {
            contentFrame().Navigate(xaml_typename<Editor::Rendering>(), winrt::make<RenderingPageNavigationState>(_settingsClone.GlobalSettings()));
        }
        else if (clickedItemTag == actionsTag)
        {
            if constexpr (Feature_EditableActionsPage::IsEnabled())
            {
                contentFrame().Navigate(xaml_typename<Editor::Actions>(), winrt::make<ActionsPageNavigationState>(_settingsClone));
            }
            else
            {
                auto actionsState{ winrt::make<ReadOnlyActionsPageNavigationState>(_settingsClone) };
                actionsState.OpenJson([weakThis = get_weak()](auto&&, auto&& arg) {
                    if (auto self{ weakThis.get() })
                    {
                        self->_OpenJsonHandlers(nullptr, arg);
                    }
                });
                contentFrame().Navigate(xaml_typename<Editor::ReadOnlyActions>(), actionsState);
            }
        }
        else if (clickedItemTag == globalProfileTag)
        {
            auto profileVM{ _viewModelForProfile(_settingsClone.ProfileDefaults(), _settingsClone) };
            profileVM.IsBaseLayer(true);
            _lastProfilesNavState = winrt::make<ProfilePageNavigationState>(profileVM,
                                                                            _settingsClone.GlobalSettings().ColorSchemes(),
                                                                            _lastProfilesNavState,
                                                                            *this);

            contentFrame().Navigate(xaml_typename<Editor::Profiles>(), _lastProfilesNavState);
        }
        else if (clickedItemTag == colorSchemesTag)
        {
            contentFrame().Navigate(xaml_typename<Editor::ColorSchemes>(), _colorSchemesNavState);
        }
        else if (clickedItemTag == globalAppearanceTag)
        {
            contentFrame().Navigate(xaml_typename<Editor::GlobalAppearance>(), winrt::make<GlobalAppearancePageNavigationState>(_settingsClone.GlobalSettings()));
        }
        else if (clickedItemTag == addProfileTag)
        {
            auto addProfileState{ winrt::make<AddProfilePageNavigationState>(_settingsClone) };
            addProfileState.AddNew({ get_weak(), &MainPage::_AddProfileHandler });
            contentFrame().Navigate(xaml_typename<Editor::AddProfile>(), addProfileState);
        }
    }

    // Method Description:
    // - updates the content frame to present a view of the profile page
    // - NOTE: this does not update the selected item.
    // Arguments:
    // - profile - the profile object we are getting a view of
    void MainPage::_Navigate(const Editor::ProfileViewModel& profile)
    {
        _lastProfilesNavState = winrt::make<ProfilePageNavigationState>(profile,
                                                                        _settingsClone.GlobalSettings().ColorSchemes(),
                                                                        _lastProfilesNavState,
                                                                        *this);

        // Add an event handler for when the user wants to delete a profile.
        _lastProfilesNavState.DeleteProfile({ this, &MainPage::_DeleteProfile });

        contentFrame().Navigate(xaml_typename<Editor::Profiles>(), _lastProfilesNavState);
    }

    void MainPage::OpenJsonTapped(IInspectable const& /*sender*/, Windows::UI::Xaml::Input::TappedRoutedEventArgs const& /*args*/)
    {
        const CoreWindow window = CoreWindow::GetForCurrentThread();
        const auto rAltState = window.GetKeyState(VirtualKey::RightMenu);
        const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu);
        const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) ||
                                WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down);

        const auto target = altPressed ? SettingsTarget::DefaultsFile : SettingsTarget::SettingsFile;
        _OpenJsonHandlers(nullptr, target);
    }

    void MainPage::OpenJsonKeyDown(IInspectable const& /*sender*/, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& args)
    {
        if (args.Key() == VirtualKey::Enter || args.Key() == VirtualKey::Space)
        {
            const auto target = args.KeyStatus().IsMenuKeyDown ? SettingsTarget::DefaultsFile : SettingsTarget::SettingsFile;
            _OpenJsonHandlers(nullptr, target);
        }
    }

    void MainPage::SaveButton_Click(IInspectable const& /*sender*/, RoutedEventArgs const& /*args*/)
    {
        _settingsClone.WriteSettingsToDisk();
    }

    void MainPage::ResetButton_Click(IInspectable const& /*sender*/, RoutedEventArgs const& /*args*/)
    {
        UpdateSettings(_settingsSource);
    }

    void MainPage::_InitializeProfilesList()
    {
        const auto menuItems = SettingsNav().MenuItems();

        // Manually create a NavigationViewItem for each profile
        // and keep a reference to them in a map so that we
        // can easily modify the correct one when the associated
        // profile changes.
        for (const auto& profile : _settingsClone.AllProfiles())
        {
            if (!profile.Deleted())
            {
                auto navItem = _CreateProfileNavViewItem(_viewModelForProfile(profile, _settingsClone));
                menuItems.Append(navItem);
            }
        }

        // Top off (the end of the nav view) with the Add Profile item
        MUX::Controls::NavigationViewItem addProfileItem;
        addProfileItem.Content(box_value(RS_(L"Nav_AddNewProfile/Content")));
        addProfileItem.Tag(box_value(addProfileTag));

        FontIcon icon;
        // This is the "Add" symbol
        icon.Glyph(L"\xE710");
        addProfileItem.Icon(icon);

        menuItems.Append(addProfileItem);
    }

    void MainPage::_CreateAndNavigateToNewProfile(const uint32_t index, const Model::Profile& profile)
    {
        const auto newProfile{ profile ? profile : _settingsClone.CreateNewProfile() };
        const auto profileViewModel{ _viewModelForProfile(newProfile, _settingsClone) };
        const auto navItem{ _CreateProfileNavViewItem(profileViewModel) };
        SettingsNav().MenuItems().InsertAt(index, navItem);

        // Select and navigate to the new profile
        SettingsNav().SelectedItem(navItem);
        _Navigate(profileViewModel);
    }

    MUX::Controls::NavigationViewItem MainPage::_CreateProfileNavViewItem(const Editor::ProfileViewModel& profile)
    {
        MUX::Controls::NavigationViewItem profileNavItem;
        profileNavItem.Content(box_value(profile.Name()));
        profileNavItem.Tag(box_value<Editor::ProfileViewModel>(profile));

        const auto iconSource{ IconPathConverter::IconSourceWUX(profile.Icon()) };
        WUX::Controls::IconSourceElement icon;
        icon.IconSource(iconSource);
        profileNavItem.Icon(icon);

        // Update the menu item when the icon/name changes
        auto weakMenuItem{ make_weak(profileNavItem) };
        profile.PropertyChanged([weakMenuItem](const auto&, const WUX::Data::PropertyChangedEventArgs& args) {
            if (auto menuItem{ weakMenuItem.get() })
            {
                const auto& tag{ menuItem.Tag().as<Editor::ProfileViewModel>() };
                if (args.PropertyName() == L"Icon")
                {
                    const auto iconSource{ IconPathConverter::IconSourceWUX(tag.Icon()) };
                    WUX::Controls::IconSourceElement icon;
                    icon.IconSource(iconSource);
                    menuItem.Icon(icon);
                }
                else if (args.PropertyName() == L"Name")
                {
                    menuItem.Content(box_value(tag.Name()));
                }
            }
        });
        return profileNavItem;
    }

    void MainPage::_DeleteProfile(const IInspectable /*sender*/, const Editor::DeleteProfileEventArgs& args)
    {
        // Delete profile from settings model
        const auto guid{ args.ProfileGuid() };
        auto profileList{ _settingsClone.AllProfiles() };
        for (uint32_t i = 0; i < profileList.Size(); ++i)
        {
            if (profileList.GetAt(i).Guid() == guid)
            {
                profileList.RemoveAt(i);
                break;
            }
        }

        // remove selected item
        uint32_t index;
        auto selectedItem{ SettingsNav().SelectedItem() };
        auto menuItems{ SettingsNav().MenuItems() };
        menuItems.IndexOf(selectedItem, index);
        menuItems.RemoveAt(index);

        // navigate to the profile next to this one
        const auto newSelectedItem{ menuItems.GetAt(index < menuItems.Size() - 1 ? index : index - 1) };
        SettingsNav().SelectedItem(newSelectedItem);
        _Navigate(newSelectedItem.try_as<MUX::Controls::NavigationViewItem>().Tag().try_as<Editor::ProfileViewModel>());
    }

    bool MainPage::ShowBaseLayerMenuItem() const noexcept
    {
        return Feature_ShowProfileDefaultsInSettings::IsEnabled();
    }
}
