/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   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 .
 */

#include <o3tl/safeint.hxx>
#include <sfx2/sidebar/FocusManager.hxx>
#include <sfx2/sidebar/Deck.hxx>
#include <sfx2/sidebar/Panel.hxx>
#include <sidebar/DeckTitleBar.hxx>
#include <sidebar/PanelTitleBar.hxx>
#include <sidebar/TitleBar.hxx>
#include <utility>
#include <vcl/event.hxx>
#include <vcl/weld.hxx>

namespace sfx2::sidebar {

FocusManager::FocusLocation::FocusLocation (const PanelComponent eComponent, const sal_Int32 nIndex)
    : meComponent(eComponent),
      mnIndex(nIndex)
{
}

FocusManager::FocusManager(std::function<void(const Panel&)> aShowPanelFunctor)
    : mpDeckTitleBar(nullptr),
      maShowPanelFunctor(std::move(aShowPanelFunctor))
{
}

FocusManager::~FocusManager()
{
    Clear();
}

void FocusManager::GrabFocus()
{
    FocusDeckTitle();
}

void FocusManager::GrabFocusPanel()
{
    FocusPanel(0, false);
}

void FocusManager::Clear()
{
    SetDeck(nullptr);
    ClearPanels();
    ClearButtons();
}

void FocusManager::ClearPanels()
{
    SharedPanelContainer aPanels;
    aPanels.swap(maPanels);
    for (auto const& panel : aPanels)
    {
        if (panel->GetTitleBar())
        {
            UnregisterWindow(panel->GetTitleBar()->GetToolBox());
            UnregisterWindow(panel->GetTitleBar()->GetExpander());
        }

        weld::Container* pContents = panel->GetContents();
        UnregisterWindow(*pContents);
    }
}

void FocusManager::ClearButtons()
{
    std::vector<weld::Widget*> aButtons;
    aButtons.swap(maButtons);
    for (auto const& button : aButtons)
    {
        UnregisterWindow(*button);
    }
}

void FocusManager::SetDeck(Deck* pDeck)
{
    DeckTitleBar* pDeckTitleBar = pDeck ? pDeck->GetTitleBar() : nullptr;
    if (mpDeckTitleBar != nullptr)
        UnregisterWindow(mpDeckTitleBar->GetToolBox());
    mxDeck = pDeck;
    mpDeckTitleBar = pDeckTitleBar;
    if (mpDeckTitleBar != nullptr)
        RegisterWindow(mpDeckTitleBar->GetToolBox());
}

void FocusManager::SetPanels (const SharedPanelContainer& rPanels)
{
    ClearPanels();
    for (auto const& panel : rPanels)
    {
        if (panel->GetTitleBar())
        {
            RegisterWindow(panel->GetTitleBar()->GetToolBox());
            RegisterWindow(panel->GetTitleBar()->GetExpander());
        }

        // Register also as key event listener at the panel.
        weld::Container* pContents = panel->GetContents();
        RegisterWindow(*pContents);

        maPanels.emplace_back(panel);
    }
}

void FocusManager::SetButtons(const std::vector<weld::Widget*>& rButtons)
{
    ClearButtons();
    for (auto const& button : rButtons)
    {
        RegisterWindow(*button);
        maButtons.emplace_back(button);
    }
}

void FocusManager::RegisterWindow(weld::Widget& rWidget)
{
    UnregisterWindow(rWidget); // explicitly unset key press handler so we can reconnect without warnings
    rWidget.connect_key_press(LINK(this, FocusManager, KeyInputHdl));
}

void FocusManager::UnregisterWindow(weld::Widget& rWidget)
{
    rWidget.connect_key_press(Link<const KeyEvent&, bool>());
}

FocusManager::FocusLocation FocusManager::GetFocusLocation() const
{
    // Check the deck title.
    if (mpDeckTitleBar && mpDeckTitleBar->GetToolBox().has_focus())
        return FocusLocation(PC_DeckToolBox, -1);

    // Search the panels.
    for (size_t nIndex = 0; nIndex < maPanels.size(); ++nIndex)
    {
        PanelTitleBar* pTitleBar = maPanels[nIndex]->GetTitleBar();
        if (!pTitleBar)
            continue;
        if (pTitleBar->GetExpander().has_focus())
            return FocusLocation(PC_PanelTitle, nIndex);
        if (pTitleBar->GetToolBox().has_focus())
            return FocusLocation(PC_PanelToolBox, nIndex);
        weld::Container* pContents = maPanels[nIndex]->GetContents();
        if (pContents->has_child_focus())
            return FocusLocation(PC_PanelContent, nIndex);
    }

    // Search the buttons.
    for (size_t nIndex=0; nIndex < maButtons.size(); ++nIndex)
    {
        if (maButtons[nIndex]->has_focus())
            return FocusLocation(PC_TabBar, nIndex);
    }
    return FocusLocation(PC_None, -1);
}

void FocusManager::FocusDeckTitle()
{
    if (mpDeckTitleBar != nullptr)
    {
        if (mpDeckTitleBar->GetToolBox().get_n_items() > 0)
        {
            weld::Toolbar& rToolBox = mpDeckTitleBar->GetToolBox();
            rToolBox.grab_focus();
        }
        else
            FocusPanel(0, false);
    }
    else
        FocusPanel(0, false);
}

bool FocusManager::IsDeckTitleVisible() const
{
    return mpDeckTitleBar != nullptr && mpDeckTitleBar->GetVisible();
}

void FocusManager::FocusPanel (
    const sal_Int32 nPanelIndex,
    const bool bFallbackToDeckTitle)
{
    if (nPanelIndex<0 || o3tl::make_unsigned(nPanelIndex)>=maPanels.size())
    {
        if (bFallbackToDeckTitle)
            FocusDeckTitle();
        return;
    }

    Panel& rPanel (*maPanels[nPanelIndex]);
    PanelTitleBar* pTitleBar = rPanel.GetTitleBar();
    if (pTitleBar && pTitleBar->GetVisible())
    {
        rPanel.SetExpanded(true);
        pTitleBar->GetExpander().grab_focus();
    }
    // Fallback to deck title should only be applicable when there is more than one panel,
    // or else it will never be possible to enter the panel contents when there's a single panel
    // without a titlebar and expander
    else if (bFallbackToDeckTitle && maPanels.size() > 1)
    {
        // The panel title is not visible, fall back to the deck
        // title.
        // Make sure that the desk title is visible here to prevent a
        // loop when both the title of panel 0 and the deck title are
        // not present.
        if (IsDeckTitleVisible())
            FocusDeckTitle();
        else
            FocusPanelContent(nPanelIndex);
    }
    else
        FocusPanelContent(nPanelIndex);

    if (maShowPanelFunctor)
        maShowPanelFunctor(rPanel);
}

void FocusManager::FocusPanelContent(const sal_Int32 nPanelIndex)
{
    if (!maPanels[nPanelIndex]->IsExpanded())
        maPanels[nPanelIndex]->SetExpanded(true);

    weld::Container* pContents = maPanels[nPanelIndex]->GetContents();
    pContents->child_grab_focus();
}

void FocusManager::FocusButton (const sal_Int32 nButtonIndex)
{
    maButtons[nButtonIndex]->grab_focus();
}

void FocusManager::MoveFocusInsidePanel (
    const FocusLocation& rFocusLocation,
    const sal_Int32 nDirection)
{
    const bool bHasToolBoxItem (
        maPanels[rFocusLocation.mnIndex]->GetTitleBar()->GetToolBox().get_n_items() > 0);
    switch (rFocusLocation.meComponent)
    {
        case  PC_PanelTitle:
            if (nDirection > 0 && bHasToolBoxItem)
                maPanels[rFocusLocation.mnIndex]->GetTitleBar()->GetToolBox().grab_focus();
            else
                FocusPanelContent(rFocusLocation.mnIndex);
            break;

        case PC_PanelToolBox:
            if (nDirection < 0 && bHasToolBoxItem)
                maPanels[rFocusLocation.mnIndex]->GetTitleBar()->GetExpander().grab_focus();
            else
                FocusPanelContent(rFocusLocation.mnIndex);
            break;

        default: break;
    }
}

bool FocusManager::HandleKeyEvent(
    const vcl::KeyCode& rKeyCode,
    const FocusLocation& aLocation)
{
    bool bConsumed = false;

    switch (rKeyCode.GetCode())
    {
        case KEY_ESCAPE:
            switch (aLocation.meComponent)
            {
                case PC_TabBar:
                case PC_DeckToolBox:
                case PC_PanelTitle:
                case PC_PanelToolBox:
                {
                    if (mxDeck)
                    {
                        mxDeck->GrabFocusToDocument();
                        bConsumed = true;
                    }
                    break;
                }
                case PC_PanelContent:
                    // Return focus to tab bar sidebar settings button or panel title.
                    if ((!IsDeckTitleVisible() && maPanels.size() == 1) ||
                        (!maPanels[aLocation.mnIndex]->GetTitleBar()->GetVisible()))
                        FocusButton(0);
                    else
                        FocusPanel(aLocation.mnIndex, true);

                    bConsumed = true;
                    break;
                default:
                    break;
            }
            return bConsumed;

        case KEY_RETURN:
            switch (aLocation.meComponent)
            {
                case PC_PanelTitle:
                    // Enter the panel.
                    FocusPanelContent(aLocation.mnIndex);
                    bConsumed = true;
                    break;

                default:
                    break;
            }
            return bConsumed;

        case KEY_TAB:
        {
            const sal_Int32 nDirection (
                rKeyCode.IsShift()
                    ? -1
                    : +1);
            switch (aLocation.meComponent)
            {
                case PC_PanelTitle:
                case PC_PanelToolBox:
                    if (rKeyCode.IsShift())
                        break;
                    MoveFocusInsidePanel(aLocation, nDirection);
                    bConsumed = true;
                    break;

                case PC_DeckToolBox:
                    {
                        // Moves to the first deck activation button that is visible and sensitive
                        sal_Int32 nIndex(0);
                        sal_Int32 nButtons(maButtons.size());
                        if (nButtons > 1)
                        {
                            nIndex = 1;
                            // Finds the next visible button that is sensitive
                            while((!maButtons[nIndex]->get_visible() ||
                                   !maButtons[nIndex]->get_sensitive()) && ++nIndex < nButtons);
                            // Wrap to the menu button when going past the last button
                            if (nIndex >= nButtons)
                                nIndex = 0;
                        }
                        FocusButton(nIndex);
                        bConsumed = true;
                    }
                    break;

                case PC_TabBar:
                    if (rKeyCode.IsShift())
                    {
                        if (IsDeckTitleVisible())
                            FocusDeckTitle();
                        else
                            FocusPanel(0, true);
                    }
                    else
                        FocusPanel(0, true);
                    bConsumed = true;
                    break;

                default:
                    break;
            }
            break;
        }

        case KEY_LEFT:
        case KEY_UP:
            switch (aLocation.meComponent)
            {
                case PC_PanelTitle:
                case PC_PanelToolBox:
                    // Go to previous panel or the deck title.
                    if (aLocation.mnIndex > 0)
                        FocusPanel(aLocation.mnIndex-1, true);
                    else if (IsDeckTitleVisible())
                        FocusDeckTitle();
                    else
                    {
                        // Set focus to the last visible sensitive button.
                        sal_Int32 nIndex(maButtons.size()-1);
                        while((!maButtons[nIndex]->get_visible() ||
                               !maButtons[nIndex]->get_sensitive()) && --nIndex > 0);
                        FocusButton(nIndex);
                    }
                    bConsumed = true;
                    break;

                case PC_TabBar:
                    {
                        if (rKeyCode.GetCode() == KEY_LEFT)
                            break;

                        sal_Int32 nIndex;
                        if (aLocation.mnIndex <= 0)
                            nIndex = maButtons.size() - 1;
                        else
                            nIndex = aLocation.mnIndex - 1;

                        // Finds the previous visible sensitive button
                        while((!maButtons[nIndex]->get_visible() ||
                               !maButtons[nIndex]->get_sensitive()) && --nIndex > 0);
                        FocusButton(nIndex);
                        bConsumed = true;
                    }
                    break;

                default:
                    break;
            }
            break;

        case KEY_RIGHT:
        case KEY_DOWN:
            switch(aLocation.meComponent)
            {
                case PC_PanelTitle:
                case PC_PanelToolBox:
                    // Go to next panel.
                    if (aLocation.mnIndex < static_cast<sal_Int32>(maPanels.size())-1)
                        FocusPanel(aLocation.mnIndex+1, false);
                    else
                        FocusButton(0);
                    bConsumed = true;
                    break;

                case PC_TabBar:
                    {
                        if (rKeyCode.GetCode() == KEY_RIGHT)
                            break;

                        sal_Int32 nButtons(maButtons.size());

                        sal_Int32 nIndex = aLocation.mnIndex + 1;
                        if (nIndex >= nButtons)
                            nIndex = 0;

                        // Finds the next visible sensitive button
                        while((!maButtons[nIndex]->get_visible() ||
                               !maButtons[nIndex]->get_sensitive()) && ++nIndex < nButtons);
                        // Wrap to the menu button when going past the last button
                        if (nIndex >= nButtons)
                            nIndex = 0;
                        FocusButton(nIndex);
                        bConsumed = true;
                    }
                    break;

                default:
                    break;
            }
            break;
    }
    return bConsumed;
}

IMPL_LINK(FocusManager, KeyInputHdl, const KeyEvent&, rKeyEvent, bool)
{
    return HandleKeyEvent(rKeyEvent.GetKeyCode(), GetFocusLocation());
}

} // end of namespace sfx2::sidebar

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
