/*
  This file is part of KDDockWidgets.

  SPDX-FileCopyrightText: 2020 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
  Author: Sérgio Martins <sergio.martins@kdab.com>

  SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only

  Contact KDAB at <info@kdab.com> for commercial licensing options.
*/

#include "TitleBar.h"
#include "TitleBar_p.h"
#include "Config.h"
#include "ViewFactory.h"
#include "View.h"
#include "WindowBeingDragged_p.h"
#include "Utils_p.h"
#include "Logging_p.h"
#include "Group_p.h"

#include "views/TitleBarViewInterface.h"
#include "DockWidget_p.h"
#include "FloatingWindow.h"
#include "DockRegistry.h"
#include "FloatingWindow_p.h"
#include "TabBar.h"
#include "MainWindow.h"
#include "MDILayout.h"
#include "Stack.h"

#ifdef KDDW_FRONTEND_QT
#include <QTimer>
#endif

#include <utility>

using namespace KDDockWidgets;
using namespace KDDockWidgets::Core;


TitleBar::TitleBar(Group *parent)
    : Controller(
          ViewType::TitleBar,
          Config::self().viewFactory()->createTitleBar(this, parent ? parent->view() : nullptr))
    , Draggable(view())
    , d(new Private())
    , m_group(parent)
    , m_floatingWindow(nullptr)
    , m_supportsAutoHide((Config::self().flags() & Config::Flag_TitleBarShowAutoHide) == Config::Flag_TitleBarShowAutoHide)
    , m_isStandalone(false)
{
    init();
    d->numDockWidgetsChangedConnection = m_group->dptr()->numDockWidgetsChanged.connect([this] {
        updateCloseButton();
        d->numDockWidgetsChanged.emit();
    });

    d->isFocusedChangedConnection = m_group->dptr()->isFocusedChanged.connect([this] {
        d->isFocusedChanged.emit();
    });

    d->isInMainWindowChangedConnection = m_group->dptr()->isInMainWindowChanged.connect([this] {
        updateAutoHideButton();
    });
}

TitleBar::TitleBar(FloatingWindow *parent)
    : Controller(
          ViewType::TitleBar,
          Config::self().viewFactory()->createTitleBar(this, parent ? parent->view() : nullptr))
    , Draggable(view())
    , d(new Private())
    , m_group(nullptr)
    , m_floatingWindow(parent)
    , m_supportsAutoHide((Config::self().flags() & Config::Flag_TitleBarShowAutoHide) == Config::Flag_TitleBarShowAutoHide)
    , m_isStandalone(false)
{
    init();
    auto fwPrivate = m_floatingWindow->dptr();
    fwPrivate->numGroupsChanged.connect([this] { updateButtons(); });
    fwPrivate->numDockWidgetsChanged.connect([this] { d->numDockWidgetsChanged.emit(); });
    fwPrivate->windowStateChanged.connect([this] { updateMaximizeButton(); });
    fwPrivate->activatedChanged.connect([this] { d->isFocusedChanged.emit(); });
}

TitleBar::TitleBar(Core::View *view)
    : Controller(ViewType::TitleBar, view)
    , Draggable(view, /*enabled=*/false)
    , d(new Private())
    , m_group(nullptr)
    , m_floatingWindow(nullptr)
    , m_supportsAutoHide(false)
    , m_isStandalone(true)
{
}

void TitleBar::init()
{
    view()->init();

    d->isFocusedChanged.connect([this] {
        // repaint
        view()->update();
    });

    updateButtons();

#ifdef KDDW_FRONTEND_QT
    // Auto-hide not supported in flutter yet
    QTimer::singleShot(0, this, &TitleBar::updateAutoHideButton); // have to wait after the group is
                                                                  // constructed
#endif
}

TitleBar::~TitleBar()
{
    delete d;
}

bool TitleBar::titleBarIsFocusable() const
{
    return Config::self().flags() & Config::Flag_TitleBarIsFocusable;
}


MainWindow *TitleBar::mainWindow() const
{
    if (m_floatingWindow || m_isStandalone)
        return nullptr;

    if (m_group)
        return m_group->mainWindow();

    KDDW_ERROR("null group and null floating window");
    return nullptr;
}

bool TitleBar::isMDI() const
{
    auto p = view()->asWrapper();
    while (p) {
        if (p->is(ViewType::MDILayout))
            return true;

        if (p->is(ViewType::DropArea)) {
            // Note that the TitleBar can be inside a DropArea that's inside a MDIArea
            // so we need this additional check
            return false;
        }

        p = p->parentView();
    }

    return false;
}

QString TitleBar::title() const
{
    return m_title;
}

Icon TitleBar::icon() const
{
    return m_icon;
}

bool TitleBar::onDoubleClicked()
{
    if (Config::self().flags() & Config::Flag_DisableDoubleClick) {
        return false;
    } else if ((Config::self().flags() & Config::Flag_DoubleClickMaximizes) && m_floatingWindow) {
        // Not using isFloating(), as that can be a dock widget nested in a floating window. By
        // convention it's floating, but it's not the title bar of the top-level window.
        toggleMaximized();
        return true;
    } else if (supportsFloatUnfloat()) {
        onFloatClicked();
        return true;
    }

    return false;
}

bool TitleBar::floatButtonVisible() const
{
    return m_floatButtonVisible;
}

bool TitleBar::maximizeButtonVisible() const
{
    return m_maximizeButtonVisible;
}

bool TitleBar::supportsFloatUnfloat() const
{
    if (m_isStandalone)
        return false; // not applicable

    if (DockWidget *dw = singleDockWidget()) {
        // Don't show the dock/undock button if the window is not dockable
        if (dw->options() & DockWidgetOption_NotDockable)
            return false;
    }

    // If we have a floating window with nested dock widgets we can't re-attach, because we don't
    // know where to
    return !m_floatingWindow || m_floatingWindow->hasSingleGroup();
}

bool TitleBar::supportsFloatingButton() const
{
    auto flags = Config::self().flags();
    if (flags & Config::Flag_TitleBarHasMaximizeButton) {
        // Apps having a maximize/restore button traditionally don't have a floating one,
        // QDockWidget style only has floating and no maximize/restore.
        // We can add an option later if we need them to co-exist
        return false;
    }

    if (flags & Config::Flag_TitleBarNoFloatButton) {
        // Was explicitly disabled
        return false;
    }

    return supportsFloatUnfloat();
}

bool TitleBar::supportsMaximizeButton() const
{
    return m_floatingWindow && m_floatingWindow->supportsMaximizeButton();
}

bool TitleBar::supportsMinimizeButton() const
{
    return m_floatingWindow && m_floatingWindow->supportsMinimizeButton();
}

bool TitleBar::supportsAutoHideButton() const
{
    // Only dock widgets docked into the MainWindow can minimize
    return m_supportsAutoHide && m_group && (m_group->isInMainWindow() || m_group->isOverlayed());
}

#ifdef DOCKS_TESTING_METHODS
bool TitleBar::isFloatButtonVisible() const
{
    return dynamic_cast<Core::TitleBarViewInterface *>(view())->isFloatButtonVisible();
}

bool TitleBar::isCloseButtonVisible() const
{
    return dynamic_cast<Core::TitleBarViewInterface *>(view())->isCloseButtonVisible();
}

bool TitleBar::isCloseButtonEnabled() const
{
    return dynamic_cast<Core::TitleBarViewInterface *>(view())->isCloseButtonEnabled();
}
#endif

bool TitleBar::hasIcon() const
{
    return !m_icon.isNull();
}

Core::Group *TitleBar::group() const
{
    return m_group;
}

Core::FloatingWindow *TitleBar::floatingWindow() const
{
    return m_floatingWindow;
}

void TitleBar::focus(Qt::FocusReason reason)
{
    if (!(Config::self().flags() & Config::Flag_TitleBarIsFocusable))
        return;

    if (m_group) {
        m_group->FocusScope::focus(reason);
    } else if (m_floatingWindow) {
        m_floatingWindow->focus(reason);
    }
}

void TitleBar::updateButtons()
{
    updateCloseButton();
    updateFloatButton();
    updateMaximizeButton();

    const bool isEnabled = true;
    const bool minimizeVisible = supportsMinimizeButton() && !buttonIsUserHidden(TitleBarButtonType::Minimize, isEnabled);
    d->minimizeButtonChanged.emit(minimizeVisible, isEnabled);

    updateAutoHideButton();
}

void TitleBar::updateAutoHideButton()
{
    TitleBarButtonType type = TitleBarButtonType::AutoHide;

    if (const Core::Group *group = this->group()) {
        if (group->isOverlayed())
            type = TitleBarButtonType::UnautoHide;
    }

    const bool isEnabled = true;
    const bool visible = m_supportsAutoHide && !buttonIsUserHidden(type, isEnabled) && !m_floatingWindow;
    d->autoHideButtonChanged.emit(visible, isEnabled, type);
}

void TitleBar::updateMaximizeButton()
{
    m_maximizeButtonVisible = false;
    m_maximizeButtonType = TitleBarButtonType::Maximize;

    if (auto fw = floatingWindow()) {
        m_maximizeButtonType =
            fw->view()->isMaximized() ? TitleBarButtonType::Normal : TitleBarButtonType::Maximize;
        m_maximizeButtonVisible = supportsMaximizeButton();
    }
    const bool isEnabled = true;
    m_maximizeButtonVisible = m_maximizeButtonVisible && !buttonIsUserHidden(m_maximizeButtonType, isEnabled);
    d->maximizeButtonChanged.emit(m_maximizeButtonVisible, isEnabled, m_maximizeButtonType);
}

void TitleBar::updateCloseButton()
{
    const bool anyNonClosable = group()
        ? group()->anyNonClosable()
        : (floatingWindow() ? floatingWindow()->anyNonClosable() : false);

    const bool isEnabled = !anyNonClosable;
    setCloseButtonEnabled(isEnabled);
    setCloseButtonVisible(!buttonIsUserHidden(TitleBarButtonType::Close, isEnabled));
}

void TitleBar::toggleMaximized()
{
    if (!m_floatingWindow)
        return;

    if (m_floatingWindow->view()->isMaximized())
        m_floatingWindow->view()->showNormal();
    else
        m_floatingWindow->view()->showMaximized();
}

bool TitleBar::isOverlayed() const
{
    return m_group && m_group->isOverlayed();
}

void TitleBar::setCloseButtonEnabled(bool enabled)
{
    if (enabled != m_closeButtonEnabled) {
        m_closeButtonEnabled = enabled;
        d->closeButtonChanged.emit(m_closeButtonVisible, enabled);
    }
}

void TitleBar::setCloseButtonVisible(bool visible)
{
    if (visible != m_closeButtonVisible) {
        m_closeButtonVisible = visible;
        d->closeButtonChanged.emit(m_closeButtonVisible, m_closeButtonEnabled);
    }
}

void TitleBar::setFloatButtonVisible(bool visible)
{
    if (visible != m_floatButtonVisible) {
        m_floatButtonVisible = visible;
        d->floatButtonVisibleChanged.emit(visible);
    }
}

void TitleBar::setFloatButtonToolTip(const QString &tip)
{
    if (tip != m_floatButtonToolTip) {
        m_floatButtonToolTip = tip;
        d->floatButtonToolTipChanged.emit(tip);
    }
}

void TitleBar::setTitle(const QString &title)
{
    if (title != m_title) {
        m_title = title;
        view()->update();
        d->titleChanged.emit();
    }
}

void TitleBar::setIcon(const Icon &icon)
{
    m_icon = icon;
    d->iconChanged.emit();
}

void TitleBar::onCloseClicked()
{
    CloseReasonSetter reason(CloseReason::TitleBarCloseButton);

    const bool closeOnlyCurrentTab = Config::self().flags() & Config::Flag_CloseOnlyCurrentTab;

    if (m_group) {
        if (closeOnlyCurrentTab) {
            if (auto dw = m_group->currentDockWidget()) {
                dw->view()->close();
            } else {
                // Doesn't happen
                KDDW_ERROR("Group with no dock widgets");
            }
        } else {
            if (m_group->isTheOnlyGroup() && m_group->isInFloatingWindow()) {
                m_group->view()->d->closeRootView();
            } else {
                m_group->view()->close();
            }
        }
    } else if (m_floatingWindow) {

        if (closeOnlyCurrentTab) {
            if (Group *f = m_floatingWindow->singleFrame()) {
                if (DockWidget *dw = f->currentDockWidget()) {
                    dw->view()->close();
                } else {
                    // Doesn't happen
                    KDDW_ERROR("Group with no dock widgets");
                }
            } else {
                m_floatingWindow->view()->close();
            }
        } else {
            m_floatingWindow->view()->close();
        }
    } else if (m_isStandalone) {
        view()->d->closeRootView();
    }
}

void TitleBar::onFloatClicked()
{
    const DockWidget::List dockWidgets = this->dockWidgets();
    if (isFloating()) {
        // Let's dock it

        if (dockWidgets.isEmpty()) {
            KDDW_ERROR("TitleBar::onFloatClicked: empty list. Shouldn't happen");
            return;
        }

        if (dockWidgets.size() == 1) {
            // Case 1: Single dockwidget floating
            dockWidgets[0]->setFloating(false);
        } else {
            // Case 2: Multiple dockwidgets are tabbed together and floating
            // Possible improvement: Just reuse the whole group and put it back.
            // The group currently doesn't remember the position in the main window
            // so use an hack for now

            if (!dockWidgets.isEmpty()) { // could be empty during destruction, maybe
                if (!dockWidgets.constFirst()->hasPreviousDockedLocation()) {
                    // Don't attempt, there's no previous docked location
                    return;
                }

                Group *group = dockWidgets[0]->d->group();

                // suppress "isFloatingChanged" signals, as we're doing the float/unfloat hack
                Group::s_inFloatHack = group ? group->layoutItem() : nullptr;

                int i = 0;
                DockWidget *current = nullptr;
                for (auto dock : std::as_const(dockWidgets)) {
                    if (!current && dock->isCurrentTab())
                        current = dock;

                    dock->setFloating(true);
                    dock->dptr()->m_lastPositions->m_tabIndex = i;
                    dock->setFloating(false);
                    ++i;
                }
                Group::s_inFloatHack = nullptr;

                // Restore the current tab
                if (current)
                    current->setAsCurrentTab();
            }
        }
    } else {
        // Let's float it
        if (dockWidgets.size() == 1) {
            // If there's a single dock widget, just call DockWidget::setFloating(true). The only
            // difference is that it has logic for using the last used geometry for the floating
            // window
            dockWidgets[0]->setFloating(true);
        } else {
            makeWindow();
        }
    }
}

void TitleBar::onMaximizeClicked()
{
    toggleMaximized();
}

void TitleBar::onMinimizeClicked()
{
    if (!m_floatingWindow)
        return;

    if (m_floatingWindow->isUtilityWindow()) {
        // Qt::Tool windows don't appear in the task bar.
        // Unless someone tells me a good reason to allow this situation.
        return;
    }

    m_floatingWindow->view()->showMinimized();
}

void TitleBar::onAutoHideClicked()
{
    if (!m_group) {
        // Doesn't happen
        KDDW_ERROR("Minimize not supported on floating windows");
        return;
    }

    const auto &dockwidgets = m_group->dockWidgets();
    if (isOverlayed() && dockwidgets.size() != 1) {
        // Doesn't happen
        KDDW_ERROR("TitleBar::onAutoHideClicked: There can only be a single dock widget per titlebar overlayed");
        return;
    }

    const bool groupedAutoHide = Config::hasFlag(Config::Flag_AutoHideAsTabGroups);
    const auto currentDw = m_group->currentDockWidget();
    auto registry = DockRegistry::self();

    if (isOverlayed()) { // Restore it:
        auto dw = dockwidgets.first();
        MainWindow *mainWindow = dw->mainWindow();
        auto sideBarGroup = groupedAutoHide ? registry->sideBarGroupingFor(dw) : DockWidget::List();
        if (sideBarGroup.isEmpty()) {
            mainWindow->restoreFromSideBar(dw);
        } else {
            // Config::Flag_AutoHideAsTabGroups case. Restore its friends too
            for (auto it = sideBarGroup.rbegin(); it != sideBarGroup.rend(); ++it) {
                mainWindow->restoreFromSideBar(*it);
            }
            dw->setAsCurrentTab();
            registry->removeSideBarGrouping(sideBarGroup);
        }
    } else { // Send it to sidebar:
        if (groupedAutoHide)
            registry->addSideBarGrouping(dockwidgets);


        CloseReasonSetter reason(CloseReason::MovedToSideBar);
        for (DockWidget *dw : dockwidgets) {
            if (groupedAutoHide || dw == currentDw)
                dw->moveToSideBar();
        }
    }
}

bool TitleBar::closeButtonEnabled() const
{
    return m_closeButtonEnabled;
}

std::unique_ptr<WindowBeingDragged> TitleBar::makeWindow()
{
    if (m_isStandalone)
        return {}; // not applicable

    if (!isVisible() && view()->rootView()->controller()->isVisible()
        && !(Config::self().flags() & Config::Flag_ShowButtonsOnTabBarIfTitleBarHidden)) {

        // When using Flag_ShowButtonsOnTabBarIfTitleBarHidden we forward the call from the tab
        // bar's buttons to the title bar's buttons, just to reuse logic

        KDDW_ERROR("TitleBar::makeWindow shouldn't be called on invisible title bar this={}, root.isVisible={}", ( void * )this, view()->rootView()->isVisible());
        if (m_group) {
            KDDW_ERROR("this={}; actual={}", ( void * )this, ( void * )m_group->actualTitleBar());
        } else if (m_floatingWindow) {
            KDDW_ERROR("Has floating window with titlebar={}, isVisible={}", ( void * )m_floatingWindow->titleBar(), m_floatingWindow->isVisible());
        }

        assert(false);
        return {};
    }

    if (m_floatingWindow) {
        // We're already a floating window, no detach needed
        return std::make_unique<WindowBeingDragged>(m_floatingWindow, this);
    }

    if (FloatingWindow *fw = floatingWindow()) { // Already floating
        if (m_group->isTheOnlyGroup()) { // We don't detach. This one drags the entire window
                                         // instead.
            KDDW_DEBUG("TitleBar::makeWindow no detach needed");
            return std::make_unique<WindowBeingDragged>(fw, this);
        }
    }

    Rect r = m_group->view()->geometry();
    r.moveTopLeft(m_group->mapToGlobal(Point(0, 0)));

    auto floatingWindow = new Core::FloatingWindow(m_group, {});
    floatingWindow->setSuggestedGeometry(r, SuggestedGeometryHint_GeometryIsFromDocked);
    floatingWindow->view()->show();

    auto draggable = KDDockWidgets::usesNativeTitleBar() ? static_cast<Draggable *>(floatingWindow)
                                                         : static_cast<Draggable *>(this);
    return std::make_unique<WindowBeingDragged>(floatingWindow, draggable);
}

bool TitleBar::isWindow() const
{
    return m_floatingWindow != nullptr;
}

Core::DockWidget::List TitleBar::dockWidgets() const
{
    if (m_floatingWindow) {
        DockWidget::List result;
        const auto groups = m_floatingWindow->groups();
        for (Group *group : groups) {
            result.append(group->dockWidgets());
        }
        return result;
    }

    if (m_group)
        return m_group->dockWidgets();

    if (m_isStandalone)
        return {}; // not applicable

    KDDW_ERROR("TitleBar::dockWidget: shouldn't happen");
    return {};
}

Core::DockWidget *TitleBar::singleDockWidget() const
{
    const DockWidget::List dockWidgets = this->dockWidgets();
    return dockWidgets.isEmpty() ? nullptr : dockWidgets.first();
}

bool TitleBar::isFloating() const
{
    if (m_floatingWindow)
        return true;

    if (m_group)
        return m_group->isFloating();

    if (m_isStandalone)
        return false; // not applicable

    KDDW_ERROR("TitleBar::isFloating: shouldn't happen");
    return false;
}

bool TitleBar::isFocused() const
{
    if (m_group)
        return m_group->isFocused();
    else if (m_floatingWindow)
        return m_floatingWindow->view()->isActiveWindow();
    else if (m_isStandalone)
        return view()->isActiveWindow();

    return false;
}

void TitleBar::updateFloatButton()
{
    setFloatButtonToolTip(floatingWindow() ? tr("Dock window") : tr("Undock window"));
    setFloatButtonVisible(supportsFloatingButton() && !buttonIsUserHidden(TitleBarButtonType::Float, /*enabled=*/true));
}

QString TitleBar::floatButtonToolTip() const
{
    return m_floatButtonToolTip;
}

TabBar *TitleBar::tabBar() const
{
    if (m_floatingWindow && m_floatingWindow->hasSingleGroup()) {
        if (Group *group = m_floatingWindow->singleFrame()) {
            return group->stack()->tabBar();
        } else {
            // Shouldn't happen
            KDDW_ERROR("Expected a group");
        }

    } else if (m_group) {
        return m_group->stack()->tabBar();
    }

    return nullptr;
}

TitleBarButtonType TitleBar::maximizeButtonType() const
{
    return m_maximizeButtonType;
}

bool TitleBar::isStandalone() const
{
    return m_isStandalone;
}

bool TitleBar::buttonIsUserHidden(TitleBarButtonType type) const
{
    return d->m_userHiddenButtonTypes & type;
}

bool TitleBar::buttonIsUserHidden(TitleBarButtonType type, bool enabled) const
{
    if (buttonIsUserHidden(type))
        return true;

    if (!enabled)
        return buttonHidesIfDisabled(type);

    return false;
}

void TitleBar::setUserHiddenButtons(TitleBarButtonTypes types)
{
    if (d->m_userHiddenButtonTypes != types) {
        d->m_userHiddenButtonTypes = types;
        updateButtons();
    }
}

TitleBar::Private *TitleBar::dptr() const
{
    return d;
}

void TitleBar::setHideDisabledButtons(TitleBarButtonTypes types)
{
    if (d->m_buttonsToHideIfDisabled != types) {
        d->m_buttonsToHideIfDisabled = types;
        updateButtons();
    }
}

bool TitleBar::buttonHidesIfDisabled(TitleBarButtonType type) const
{
    return d->m_buttonsToHideIfDisabled & type;
}
