/*
  This file is part of KDDockWidgets.

  SPDX-FileCopyrightText: 2019 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 "DockWidget.h"
#include "DockWidget_p.h"
#include "DockRegistry.h"
#include "core/LayoutSaver_p.h"
#include "core/Logging_p.h"
#include "core/MDILayout.h"
#include "core/TitleBar.h"
#include "core/Group.h"
#include "core/Stack.h"
#include "core/FloatingWindow.h"
#include "core/SideBar.h"
#include "core/DropArea.h"
#include "core/TabBar.h"
#include "core/TabBar_p.h"
#include "core/MainWindow.h"
#include "core/Utils_p.h"
#include "core/WindowBeingDragged_p.h"
#include "core/Position_p.h"
#include "core/Platform_p.h"
#include "core/Action_p.h"
#include "core/DelayedCall_p.h"
#include "core/DragController_p.h"
#include "Platform.h"
#include "core/layouting/Item_p.h"

#include "Config.h"
#include "core/ViewFactory.h"
#include "core/ScopedValueRollback_p.h"

#ifdef KDDW_FRONTEND_QT
#include <QTimer>
#endif

/**
 * @file
 * @brief The DockWidget base-class that's shared between QtWidgets and QtQuick stack.
 *
 * @author Sérgio Martins \<sergio.martins@kdab.com\>
 */

using namespace KDDockWidgets;
using namespace KDDockWidgets::Core;

DockWidget::DockWidget(View *view, const QString &name, DockWidgetOptions options,
                       LayoutSaverOptions layoutSaverOptions)
    : Controller(ViewType::DockWidget, view)
    , d(new Private(name, options, layoutSaverOptions, this))
{
    DockRegistry::self()->registerDockWidget(this);

    if (name.isEmpty())
        KDDW_ERROR("Name can't be null");

    d->m_windowActivatedConnection = Platform::instance()->d->windowActivated.connect(
        &DockWidget::Private::onWindowActivated, d);
    d->m_windowDeactivatedConnection = Platform::instance()->d->windowDeactivated.connect(
        &DockWidget::Private::onWindowDeactivated, d);

    // If user called LayoutSaver::restoreLayout() before creating the dock widget, we need to
    // keep the previous known dock widget position
    if (layoutSaverOptions & LayoutSaverOption::CheckForPreviousRestore)
        LayoutSaver::Private::restorePendingPositions(this);
}

DockWidget::~DockWidget()
{
    m_inDtor = true;
    d->m_windowActivatedConnection->disconnect();
    d->m_windowDeactivatedConnection->disconnect();

    safeEmitSignal(d->aboutToDelete, this);
    DockRegistry::self()->unregisterDockWidget(this);
    delete d;
}

void DockWidget::init()
{
    d->init();
    view()->enableAttribute(Qt::WA_PendingMoveEvent, false);
}

void DockWidget::addDockWidgetAsTab(DockWidget *other, const InitialOption &option)
{
    if (other == this) {
        KDDW_ERROR("Refusing to add dock widget into itself {}", ( void * )other);
        return;
    }

    if (!other) {
        KDDW_ERROR("dock widget is null");
        return;
    }

    if (!DockRegistry::self()->affinitiesMatch(other->affinities(), d->affinities)) {
        KDDW_ERROR("Refusing to dock widget with incompatible affinity. {} {}", other->affinities(), affinities());
        return;
    }

    if ((other->options() & DockWidgetOption_NotDockable)
        || (options() & DockWidgetOption_NotDockable)) {
        KDDW_ERROR("Refusing to dock non-dockable widget {}", ( void * )other);
        return;
    }

    if (isPersistentCentralDockWidget()) {
        KDDW_ERROR("Not supported with MainWindowOption_HasCentralWidget."
                   "MainWindowOption_HasCentralWidget can only have 1 widget in the center."
                   "Use MainWindowOption_HasCentralFrame instead, which is similar but supports "
                   "tabbing.");
        return;
    }

    Core::Group *group = d->group();

    if (group) {
        if (group->containsDockWidget(other)) {
            KDDW_ERROR("Already contains {}", ( void * )other);
            return;
        }
    } else {
        if (view()->isRootView()) {
            // Doesn't have a group yet
            d->morphIntoFloatingWindow();
            group = d->group();
            assert(group);
        } else {
            // Doesn't happen
            KDDW_ERROR("null group");
            return;
        }
    }

    other->setParentView(nullptr);
    group->addTab(other, option);
}

void DockWidget::addDockWidgetToContainingWindow(DockWidget *other, Location location,
                                                 DockWidget *relativeTo,
                                                 const InitialOption &initialOption)
{
    if (!other)
        return;

    if (auto mw = mainWindow()) {
        // It's inside a main window. Simply use the main window API.
        mw->addDockWidget(other, location, relativeTo, initialOption);
        return;
    }

    if (!DockRegistry::self()->affinitiesMatch(other->affinities(), d->affinities)) {
        KDDW_ERROR("Refusing to dock widget with incompatible affinity. {} {}", other->affinities(), affinities());
        return;
    }

    if ((other->options() & DockWidgetOption_NotDockable)
        || (options() & DockWidgetOption_NotDockable)) {
        KDDW_ERROR("Refusing to dock non-dockable widget {}", ( void * )other);
        return;
    }

    if (view()->isRootView())
        d->morphIntoFloatingWindow();

    if (auto fw = floatingWindow()) {
        fw->addDockWidget(other, location, relativeTo, initialOption);
    } else {
        KDDW_ERROR("Couldn't find floating nested window");
    }
}

std::shared_ptr<View> DockWidget::guestView() const
{
    return d->guest;
}

void DockWidget::setGuestView(std::shared_ptr<View> guest)
{
    if ((guest && guest->equals(d->guest)) || (!guest && !d->guest))
        return;

    if (d->guest) {
        // Unparent the old widget, we're giving back ownership
        d->guest->setParent(nullptr);
    }

    d->guest = guest;
    if (guest)
        guest->setParent(view());

    d->guestViewChanged.emit();
}

bool DockWidget::isFloating() const
{
    if (view()->isRootView())
        return true;

    // Deal with case of main window nesting:
    // Dock widget inside main window 1 inside mainwindow 2 and detach mw1, would
    // still be in a floating window, but dockwidget is still docked to the inner main window
    if (isInMainWindow())
        return false;

    auto fw = floatingWindow();
    return fw && fw->hasSingleDockWidget();
}

bool DockWidget::setFloating(bool floats)
{
    const bool alreadyFloating = isFloating();

    if (floats == alreadyFloating)
        return true; // Nothing to do

    if (!floats
        && (Config::self().internalFlags()
            & Config::InternalFlag_DontShowWhenUnfloatingHiddenWindow)
        && !isVisible()) {
        // Mimics behaviour of QDockWidget, which you might need during porting.
        // Not something we suggest though. For KDDW, setFloating(false) means dock, and that
        // implies showing.
        return false;
    }

    if (floats && isPersistentCentralDockWidget())
        return false;

    if (floats) {
        d->saveTabIndex();
        if (isTabbed()) {
            auto group = d->group();
            if (!group) {
                KDDW_ERROR("DockWidget::setFloating: Tabbed but no group exists", ( void * )this);
                assert(false);
                return false;
            }

            group->detachTab(this);
        } else {
            titleBar()->makeWindow();
        }

        auto lastGeo = d->lastPosition()->lastFloatingGeometry();
        if (lastGeo.isValid()) {
            if (auto fw = floatingWindow())
                fw->setSuggestedGeometry(lastGeo, SuggestedGeometryHint_PreserveCenter);
        }
        return true;
    } else {
        d->saveLastFloatingGeometry();
        return d->restoreToPreviousPosition();
    }
}

Action *DockWidget::toggleAction() const
{
    return d->toggleAction;
}

Action *DockWidget::floatAction() const
{
    return d->floatAction;
}

QString DockWidget::uniqueName() const
{
    return d->uniqueName();
}

QString DockWidget::title() const
{
    if (d->isMDIWrapper()) {
        // It's just a wrapper to help implementing Option_MDINestable. Return the title of the real
        // dock widget we're hosting.
        auto dropAreaGuest = d->guest ? guestView()->asDropAreaController() : nullptr;
        assert(dropAreaGuest);
        if (dropAreaGuest->hasSingleGroup()) {
            return dropAreaGuest->groups().constFirst()->title();
        } else {
            return Platform::instance()->applicationName();
        }
    }

    return d->title;
}

void DockWidget::setTitle(const QString &title)
{
    if (title != d->title) {
        d->title = title;
        d->updateTitle();
        d->titleChanged.emit(title);
    }
}

Rect DockWidget::groupGeometry() const
{
    if (Core::Group *f = d->group())
        return f->view()->geometry();

    // Means the dock widget isn't visible. Just fallback to its own geometry
    return geometry();
}

DockWidgetOptions DockWidget::options() const
{
    return d->options;
}

LayoutSaverOptions DockWidget::layoutSaverOptions() const
{
    return d->layoutSaverOptions;
}

void DockWidget::setOptions(DockWidgetOptions options)
{
    if ((d->options & DockWidgetOption_NotDockable) != (options & DockWidgetOption_NotDockable)) {
        KDDW_ERROR(
            "DockWidget::setOptions: Option_NotDockable not allowed to change. Pass via ctor only.");
        return;
    }

    if (options != d->options) {
        d->options = options;
        d->optionsChanged.emit(options);
        if (auto tb = titleBar())
            tb->updateButtons();
    }
}

bool DockWidget::isTabbed() const
{
    if (Core::Group *group = d->group()) {
        return group->alwaysShowsTabs() || group->dockWidgetCount() > 1;
    } else {
        if (!isFloating())
            KDDW_ERROR("DockWidget::isTabbed() Couldn't find any tab widget.");
        return false;
    }
}

bool DockWidget::isCurrentTab() const
{
    if (Core::Group *group = d->group()) {
        return group->currentIndex() == group->indexOfDockWidget(const_cast<DockWidget *>(this));
    } else {
        return true;
    }
}

void DockWidget::setAsCurrentTab()
{
    if (Core::Group *group = d->group())
        group->setCurrentDockWidget(this);
}

int DockWidget::tabIndex() const
{
    if (Core::Group *group = d->group())
        return group->indexOfDockWidget(this);

    return 0;
}

int DockWidget::currentTabIndex() const
{
    if (Group *group = d->group())
        return group->currentTabIndex();

    return 0;
}

void DockWidget::setIcon(const Icon &icon, IconPlaces places)
{
    if (places & IconPlace::TitleBar)
        d->titleBarIcon = icon;

    if (places & IconPlace::TabBar)
        d->tabBarIcon = icon;

    if (places & IconPlace::ToggleAction)
        d->toggleAction->setIcon(icon);

    d->iconChanged.emit();
}

Icon DockWidget::icon(IconPlace place) const
{
    if (place == IconPlace::TitleBar)
        return d->titleBarIcon;

    if (place == IconPlace::TabBar)
        return d->tabBarIcon;

    if (place == IconPlace::ToggleAction)
        return d->toggleAction->icon();

    return {};
}

void DockWidget::forceClose()
{
    d->forceClose();
}

Core::TitleBar *DockWidget::titleBar() const
{
    if (Core::Group *f = d->group())
        return f->actualTitleBar();

    return nullptr;
}

bool DockWidget::isOpen() const
{
    return d->m_isOpen;
}

Vector<QString> DockWidget::affinities() const
{
    return d->affinities;
}

void DockWidget::show()
{
    open();
}

void DockWidget::open()
{
    if (view()->isRootView()
        && (d->m_lastPositions->wasFloating() || d->m_lastPositions->lastItem(this) == nullptr)) {
        // Create the FloatingWindow already, instead of waiting for the show event.
        // This reduces flickering on some platforms
        d->morphIntoFloatingWindow();
    } else {
        d->setIsOpen(true);
    }
}

void DockWidget::raise()
{
    if (!isOpen())
        return;

    setAsCurrentTab();

    if (auto fw = floatingWindow()) {
        fw->view()->raise();
        fw->view()->activateWindow();
    } else if (Core::Group *group = d->group()) {
        if (group->isMDI())
            group->view()->raise();
    }
}

bool DockWidget::isMainWindow() const
{
    if (auto guest = guestView())
        return guest->is(ViewType::MainWindow);
    return false;
}

bool DockWidget::isInMainWindow() const
{
    return d->mainWindow() != nullptr;
}

MainWindow *DockWidget::mainWindow() const
{
    return d->mainWindow();
}

bool DockWidget::isFocused() const
{
    return DockRegistry::self()->focusedDockWidget() == this;
}

void DockWidget::setAffinityName(const QString &affinity)
{
    setAffinities({ affinity });
}

void DockWidget::setAffinities(const Vector<QString> &affinityNames)
{
    Vector<QString> affinities = affinityNames;
    affinities.removeAll(QString());

    if (d->affinities == affinities)
        return;

    if (!d->affinities.isEmpty()) {
        /// There's too many use cases to consider if we allowed this
        /// - What if dock widget is docked already, it would possibly get incompatible affinity
        /// - If it's floating, but has some main window as transient parent, then the transient parent
        /// could have incompatible affinity.
        /// - Etc.
        KDDW_ERROR("Affinity is already set, refusing to change."
                   "Submit a feature request with a good justification.");
        return;
    }

    d->affinities = affinities;
}

void DockWidget::moveToSideBar()
{
    if (MainWindow *m = mainWindow())
        m->moveToSideBar(this);
}

void DockWidget::removeFromSideBar()
{
    if (MainWindow *m = mainWindow()) {
        if (auto sb = m->sideBarForDockWidget(this)) {
            sb->removeDockWidget(this);
        }
    }
}

bool DockWidget::isOverlayed() const
{
    if (MainWindow *m = mainWindow())
        return m->overlayedDockWidget() == this;

    return false;
}

SideBarLocation DockWidget::sideBarLocation() const
{
    return DockRegistry::self()->sideBarLocationForDockWidget(this);
}

bool DockWidget::isInSideBar() const
{
    return sideBarLocation() != SideBarLocation::None;
}

bool DockWidget::hasPreviousDockedLocation() const
{
    return d->m_lastPositions->lastItem(this);
}

Size DockWidget::lastOverlayedSize() const
{
    return d->m_lastOverlayedSize;
}

Core::DockWidget *DockWidget::byName(const QString &uniqueName)
{
    return DockRegistry::self()->dockByName(uniqueName);
}

void DockWidget::setParentView_impl(View *parent)
{
    if (Core::Group *group = d->group()) {
        group->tabBar()->removeDockWidget(this);
    }

    Controller::setParentView_impl(parent);
    d->onParentChanged();
}

bool DockWidget::skipsRestore() const
{
    return d->layoutSaverOptions & LayoutSaverOption::Skip;
}

CloseReason DockWidget::lastCloseReason() const
{
    return d->m_lastCloseReason;
}

void DockWidget::setFloatingGeometry(Rect geometry)
{
    if (isOpen() && isFloating()) {
        view()->rootView()->setGeometry(geometry);
    } else {
        d->m_lastPositions->setLastFloatingGeometry(geometry);
    }
}

Core::FloatingWindow *DockWidget::floatingWindow() const
{
    if (auto fw = view()->rootView()->asFloatingWindowController())
        return fw;

    return nullptr;
}

Core::FloatingWindow *DockWidget::Private::morphIntoFloatingWindow()
{
    if (auto fw = floatingWindow())
        return fw; // Nothing to do

    if (q->view()->isRootView()) {
        Rect geo = m_lastPositions->lastFloatingGeometry();
        if (geo.isNull()) {
            geo = q->geometry();

            if (!q->view()->hasAttribute(Qt::WA_PendingMoveEvent)) { // If user already moved it,
                                                                     // we don't
                // interfere
                const Point center = defaultCenterPosForFloating();
                if (!center.isNull())
                    geo.moveCenter(center);
            }
        }

        auto group = new Core::Group();
        group->addTab(q);
        geo.setSize(geo.size().boundedTo(group->view()->maxSizeHint()));
        geo.setSize(geo.size().expandedTo(group->view()->minSize()));
        Core::FloatingWindow::ensureRectIsOnScreen(geo);
        auto floatingWindow = new Core::FloatingWindow(group, geo);

        Core::AtomicSanityChecks checks(floatingWindow->dropArea()->rootItem());
        floatingWindow->view()->show();
        setIsOpen(true);

        return floatingWindow;
    } else {
        return nullptr;
    }
}

void DockWidget::Private::maybeMorphIntoFloatingWindow()
{
    if (q->view()->isRootView() && q->isVisible())
        morphIntoFloatingWindow();
}

MDILayout *DockWidget::Private::mdiLayout() const
{
    auto p = q->view()->parentView();
    while (p) {
        // We found a layout
        if (auto mdiLayout = p->asMDILayoutController()) {
            // And it's MDI
            return mdiLayout;
        } else if (auto dropArea = p->asDropAreaController()) {
            // It's a DropArea. But maybe it's a drop area that's just helping
            // making the MDI windows accept drops (Option_MDINestable)
            if (!dropArea->isMDIWrapper())
                return nullptr;

            // It's a MDI wrapper, keep looking up.
        }

        p = p->parentView();
    }

    return nullptr;
}

bool DockWidget::Private::isMDIWrapper() const
{
    return mdiDropAreaWrapper() != nullptr;
}

DropArea *DockWidget::Private::mdiDropAreaWrapper() const
{
    if (auto dropAreaGuest = guest ? q->guestView()->asDropAreaController() : nullptr) {
        if (dropAreaGuest->isMDIWrapper())
            return dropAreaGuest;
    }

    return nullptr;
}

Core::DockWidget *DockWidget::Private::mdiDockWidgetWrapper() const
{
    if (isMDIWrapper()) {
        // We are the wrapper
        return q;
    }

    auto p = q->view()->parentView();
    while (p) {

        if (p->is(ViewType::DropArea) || p->is(ViewType::MDILayout)) {
            if (auto dropArea = p->asDropAreaController()) {
                if (dropArea->isMDIWrapper())
                    return dropArea->mdiDockWidgetWrapper();
            }

            return nullptr;
        }

        p = p->parentView();
    }

    return nullptr;
}

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

DockWidget::Private::~Private()
{
    delete toggleAction;
    delete floatAction;
}

Point DockWidget::Private::defaultCenterPosForFloating()
{
    MainWindow::List mainWindows = DockRegistry::self()->mainwindows();
    // We don't care about multiple mainwindows yet. Or, let's just say that the first one is more
    // main than the others
    MainWindow *mw = mainWindows.isEmpty() ? nullptr : mainWindows.constFirst();
    if (!mw || !q->isFloating())
        return {};

    return mw->geometry().center();
}

void DockWidget::Private::onWindowActivated(std::shared_ptr<View> rootView)
{
    if (View::equals(rootView.get(), q->view()->rootView().get()))
        windowActiveAboutToChange.emit(true);
}

void DockWidget::Private::onWindowDeactivated(std::shared_ptr<View> rootView)
{
    if (rootView->inDtor() || q->view()->inDtor())
        return;

    if (View::equals(rootView.get(), q->view()->rootView().get()))
        windowActiveAboutToChange.emit(false);
}

void DockWidget::Private::updateTitle()
{
    if (q->isFloating())
        q->view()->rootView()->setWindowTitle(title);

    toggleAction->setText(title);
}

void DockWidget::Private::toggle(bool enabled)
{
    if (Core::SideBar *sb = sideBar()) {
        // The widget is in the sidebar, let's toggle its overlayed state
        ScopedValueRollback guard(m_removingFromOverlay, true);
        sb->toggleOverlay(q);
    } else {
        // The most common case. The dock widget is not in the sidebar. just close or open it.
        if (enabled) {
            q->open();
        } else {
            q->view()->close();
        }
    }
}

void DockWidget::Private::updateToggleAction()
{
    ScopedValueRollback recursionGuard(m_updatingToggleAction,
                                       true); // Guard against recursiveness

    if (m_willUpdateActions || Group::s_inFloatHack) {
        // We're in the middle of a DropArea::addDockWidget() call. No point in triggering actions right now
        // they will be triggered at the end already.
        // Fixes Actions being triggered when using StartsHidden.
        return;
    }

    if ((q->isVisible() || group()) && !toggleAction->isChecked()) {
        toggleAction->setChecked(true);
    } else if ((!q->isVisible() && !group()) && toggleAction->isChecked()) {
        toggleAction->setChecked(false);
    }
}

void DockWidget::Private::updateFloatAction()
{
    if (m_willUpdateActions || m_removingFromOverlay || Group::s_inFloatHack)
        return;

    ScopedValueRollback recursionGuard(m_updatingFloatAction,
                                       true); // Guard against recursiveness

    if (q->isFloating()) {
        floatAction->setEnabled(m_lastPositions->lastItem(q));
        floatAction->setChecked(true);
        floatAction->setToolTip(Object::tr("Dock"));
    } else {
        floatAction->setEnabled(true);
        floatAction->setChecked(false);
        floatAction->setToolTip(Object::tr("Detach"));
    }
}

void DockWidget::Private::close()
{
    if (m_inClose)
        return;
    ScopedValueRollback guard(m_inClose, true);

    if (!m_processingToggleAction && !q->isOpen()) {
        q->setParentView(nullptr);
        return;
    }

    if (m_isPersistentCentralDockWidget)
        return;

    m_lastCloseReason = DockRegistry::self()->currentCloseReason();
    setIsOpen(false);

    // If it's overlayed and we're closing, we need to close the overlay
    if (Core::SideBar *sb = DockRegistry::self()->sideBarForDockWidget(q)) {
        auto mainWindow = sb->mainWindow();
        if (mainWindow->overlayedDockWidget() == q) {
            mainWindow->clearSideBarOverlay(/* deleteGroup=*/false);
        }
    }

    if (!m_isForceClosing && q->isFloating()
        && q->isVisible()) { // only user-closing is interesting to save the geometry
        // We check for isVisible so we don't save geometry if you call close() on an already closed
        // dock widget
        m_lastPositions->setLastFloatingGeometry(q->view()->d->windowGeometry());
    }

    if (!m_removingFromOverlay)
        saveTabIndex();

    // Do some cleaning. Widget is hidden, but we must hide the tab containing it.
    if (Core::Group *group = this->group()) {
        q->setParent(nullptr);
        q->setParentView(nullptr);
        group->removeWidget(q);

        if (Core::SideBar *sb = DockRegistry::self()->sideBarForDockWidget(q)) {
            sb->removeDockWidget(q);
        }
    }

    if (!m_isMovingToSideBar && (options & DockWidgetOption_DeleteOnClose)) {
        aboutToDeleteOnClose.emit();
        q->destroyLater();
    }
}

bool DockWidget::Private::restoreToPreviousPosition()
{
    Core::Item *item = m_lastPositions->lastItem(q);
    if (!item)
        return false;

    Layout *layout = DockRegistry::self()->layoutForItem(item);
    assert(layout);
    layout->restorePlaceholder(q, item, m_lastPositions->lastTabIndex());
    return true;
}

void DockWidget::Private::maybeRestoreToPreviousPosition()
{
    // This is called when we open a dock widget. Let's see if we have to restore it to a previous
    // position.

    Core::Item *layoutItem = m_lastPositions->lastItem(q);
    if (!layoutItem)
        return; // nothing to do, no last position

    if (m_lastPositions->wasFloating())
        return; // Nothing to do, it was floating before, now it'll just get visible

    Core::Group *group = this->group();

    if (group && group->view()->equals(DockRegistry::self()->layoutForItem(layoutItem)->view())) {
        // There's a group already. Means the DockWidget was hidden instead of closed.
        // Nothing to do, the dock widget will simply be shown
        return;
    }

    // Now we deal with the case where the DockWidget was close()ed. In this case it doesn't have a
    // parent.

    if (q->view()->parentView()) {
        // Was called due to it being made floating. Nothing to restore.
        return;
    }

    // Finally, restore it
    restoreToPreviousPosition();
}

int DockWidget::Private::currentTabIndex() const
{
    Core::Group *group = this->group();
    return group ? group->indexOfDockWidget(q) : 0;
}

void DockWidget::Private::saveTabIndex()
{
    m_lastPositions->saveTabIndex(currentTabIndex(), q->isFloating());
}

void DockWidget::Private::onParentChanged()
{
    updateToggleAction();
    updateFloatAction();

    actualTitleBarChanged.emit();
}

void DockWidget::onResize(Size)
{
    if (isOverlayed()) {
        if (auto group = d->group()) {
            d->m_lastOverlayedSize = group->view()->size();
        } else {
            KDDW_ERROR("Overlayed dock widget without group shouldn't happen");
        }
    }
}

Core::DockWidget *DockWidget::deserialize(const LayoutSaver::DockWidget::Ptr &saved)
{
    if (saved->skipsRestore())
        return nullptr;

    auto dr = DockRegistry::self();
    DockWidget *dw =
        dr->dockByName(saved->uniqueName, DockRegistry::DockByNameFlags(DockRegistry::DockByNameFlag::CreateIfNotFound) | DockRegistry::DockByNameFlag::SilentIfNotFound);
    if (dw) {
        if (auto guest = dw->guestView())
            guest->setVisible(true);
        dw->d->m_wasRestored = true;
#if defined(KDDW_FRONTEND_QT) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
        dw->setUserData(saved->userData);
#endif

        if (dw->affinities() != saved->affinities) {
            KDDW_ERROR("Affinity name changed from {} to {}", dw->affinities(), "; to", saved->affinities);
            dw->d->affinities = saved->affinities;
        }

        dw->dptr()->m_lastCloseReason = saved->lastCloseReason;
    }

    return dw;
}

void DockWidget::setUserType(int userType)
{
    d->m_userType = userType;
}

int DockWidget::userType() const
{
    return d->m_userType;
}

#if defined(KDDW_FRONTEND_QT) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void DockWidget::setUserData(const QVariantMap &userData)
{
    if (d->m_userData != userData) {
        d->m_userData = userData;
        d->userDataChanged.emit();
    }
}

QVariantMap DockWidget::userData() const
{
    return d->m_userData;
}
#endif

void DockWidget::setMDIPosition(Point pos)
{
    if (MDILayout *layout = d->mdiLayout()) {
        if (auto wrapperDW = d->mdiDockWidgetWrapper()) {
            // Case of using Option_MDINestable. We need to layout the actual top level DW
            layout->moveDockWidget(wrapperDW, pos);
        } else {
            layout->moveDockWidget(this, pos);
        }
    }
}

void DockWidget::setMDISize(Size size)
{
    if (MDILayout *layout = d->mdiLayout()) {
        if (auto wrapperDW = d->mdiDockWidgetWrapper()) {
            // Case of using Option_MDINestable. We need to layout the actual top level DW
            layout->resizeDockWidget(wrapperDW, size);
        } else {
            layout->resizeDockWidget(this, size);
        }
    }
}

void DockWidget::setMDIZ(int z)
{
    if (Group *group = d->group()) {
        if (group->isMDI())
            group->view()->setZOrder(z);
    }
}

int DockWidget::mdiZ() const
{
    if (Group *group = d->group()) {
        if (group->isMDI())
            return group->view()->zOrder();
    }

    return 0;
}

bool DockWidget::isPersistentCentralDockWidget() const
{
    return d->m_isPersistentCentralDockWidget;
}

LayoutSaver::DockWidget::Ptr DockWidget::Private::serialize() const
{
    auto ptr = LayoutSaver::DockWidget::dockWidgetForName(q->uniqueName());
    ptr->affinities = q->affinities();
    ptr->lastCloseReason = m_lastCloseReason;
#if defined(KDDW_FRONTEND_QT) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    ptr->userData = m_userData;
#endif

    return ptr;
}

void DockWidget::Private::forceClose()
{
    ScopedValueRollback rollback(m_isForceClosing, true);
    close();
}

DockWidget::Private::Private(const QString &dockName, DockWidgetOptions options_,
                             LayoutSaverOptions layoutSaverOptions_, DockWidget *qq)

    : m_uniqueName(dockName)
    , title(dockName)
    , q(qq)
    , options(options_)
    , layoutSaverOptions(layoutSaverOptions_)
    , toggleAction(Config::self().viewFactory()->createAction(q, "toggle"))
    , floatAction(Config::self().viewFactory()->createAction(q, "float"))
{
    m_toggleActionConnection = toggleAction->d->toggled.connect(
        [this](bool enabled) {
            if (!m_updatingToggleAction) { // guard against recursiveness
                toggleAction->blockSignals(true); // and don't emit spurious toggle. Like when a dock
                                                  // widget is inserted into a tab widget it might get
                                                  // hide events, ignore those. The Dock Widget is open.
                m_processingToggleAction = true;
                CloseReasonSetter reason(enabled ? CloseReason::Unspecified : CloseReason::Action);
                toggle(enabled);
                toggleAction->blockSignals(false);
                m_processingToggleAction = false;
            }
        });

    m_floatActionConnection = floatAction->d->toggled.connect([this](bool checked) {
        if (!m_updatingFloatAction) { // guard against recursiveness
            q->setFloating(checked);
        }

        KDDW_TRACE("Emitting DockWidget::isFloatingChanged({})", checked);
        isFloatingChanged.emit(checked);

        // When floating, we remove from the sidebar
        if (checked && q->isOpen()) {
            if (Core::SideBar *sb = DockRegistry::self()->sideBarForDockWidget(q)) {
                sb->mainWindow()->clearSideBarOverlay(/* deleteGroup=*/false);
                sb->removeDockWidget(q);
            }
        }
    });
}

void DockWidget::Private::addPlaceholderItem(Core::Item *item)
{
    assert(item);
    m_lastPositions->addPlaceholderItem(item);
}

Positions::Ptr &DockWidget::Private::lastPosition()
{
    return m_lastPositions;
}

Core::Group *DockWidget::Private::group() const
{
    auto p = q->view()->parentView();
    while (p) {
        if (auto group = p->asGroupController())
            return group;
        p = p->parentView();
    }
    return nullptr;
}

Core::Item *DockWidget::Private::item() const
{
    auto group = this->group();
    if (!group || group->inDtor())
        return nullptr;

    return group->layoutItem();
}

void DockWidget::Private::saveLastFloatingGeometry()
{
    if (q->isFloating() && q->isVisible()) {
        // It's getting docked, save last floating position
        lastPosition()->setLastFloatingGeometry(q->view()->d->windowGeometry());
    }
}

void DockWidget::Private::onCloseEvent(CloseEvent *e)
{
    if (m_inCloseEvent)
        return;
    ScopedValueRollback guard(m_inCloseEvent, true);

    e->accept(); // By default we accept, means DockWidget closes

    if (auto v = q->view()) {
        // Give a chance for QtWidgets::DockWidgets::closeEvent(ev) to ignore
        Platform::instance()->sendEvent(v, e);
        if (!e->isAccepted())
            return;
    }

    if (guest) {
        // Give a chance for the widget to ignore
        Platform::instance()->sendEvent(guest.get(), e);
        if (!e->isAccepted())
            return;
    }

    close();
}

void DockWidget::Private::setIsOpen(bool is)
{
    if (is == m_isOpen || m_inOpenSetter)
        return;

    ScopedValueRollback guard(m_inOpenSetter, true);

    if (!is)
        close();

    m_isOpen = is;

    if (is && !LayoutSaver::restoreInProgress()) {
        maybeRestoreToPreviousPosition();

#ifdef KDDW_FRONTEND_QT
        // Transform into a FloatingWindow if this will be a regular floating dock widget.
        QTimer::singleShot(0, q, [this] { maybeMorphIntoFloatingWindow(); });
#endif
    }

    updateToggleAction();
    updateFloatAction();

    if (is && !q->isOverlayed()) {
        // If we're open, we can't be in the sidebar without overlay, both or nothing.
        // nothing in this case
        q->removeFromSideBar();
    }

    if (!is) {
        closed.emit();
    }

    isOpenChanged.emit(is);
}

QString DockWidget::Private::uniqueName() const
{
    return m_uniqueName;
}

void DockWidget::Private::setUniqueName(const QString &name)
{
    if (name.isEmpty()) {
        KDDW_ERROR("DockWidget::Private::setUniqueName: Name is empty");
    } else {
        m_uniqueName = name;
    }
}

void DockWidget::setFloatingWindowFlags(FloatingWindowFlags flags)
{
    if (floatingWindow()) {
        KDDW_ERROR("Call this function only before having a floating window");
    } else {
        d->m_flags = flags;
    }
}

KDDockWidgets::FloatingWindowFlags DockWidget::floatingWindowFlags() const
{
    return d->m_flags;
}

Size DockWidget::sizeInLayout() const
{
    if (auto group = d->group())
        return group->size();

    return size();
}

void DockWidget::resizeInLayout(int left, int top, int right, int bottom)
{
    Item *item = d->item();
    if (!item || item->m_inDtor)
        return;

    item->requestResize(left, top, right, bottom);
}

namespace {
Draggable *draggableForDockWidget(DockWidget *dw, bool singleTab)
{
    Group *group = dw->d->group();
    if (!group) {
        KDDW_WARN("draggableForDockWidget: Expected a group");
        return nullptr;
    }

    if (singleTab && !group->hasSingleDockWidget()) {
        // Case #1: Detaching a single tab if the group has multiple tabs
        return group->tabBar();
    }

    // Case #2: Dragging a group of tabs, via titlebar
    Core::TitleBar *titleBar = dw->titleBar();
    if (titleBar && titleBar->isVisible())
        return titleBar;

    // Case #3: Dragging a group of tabs, via tab bar background. QTabBar background is actually QTabWidget (aka stack)
    return group->stack();
}
}

bool DockWidget::startDragging(bool singleTab)
{
    auto dc = DragController::instance();
    if (dc->isDragging()) {
        KDDW_WARN("DockWidget::startDragging: Dragging already ongoing");
        return false;
    }

    Draggable *const draggable = draggableForDockWidget(this, singleTab);
    if (!draggable) {
        KDDW_WARN("DockWidget::startDragging: Could not find a suitable draggable");
        return false;
    }

    Core::TabBar *tabBar = d->group()->tabBar();
    if (draggable->asView() == tabBar->view()) {
        // minor hack, so we can reuse the normal mouse interaction code path
        tabBar->dptr()->m_lastPressedDockWidget = this;
    }

    const Point globalPos = Platform::instance()->cursorPos();

    View *draggedView = draggable->asView();
    const Point offset = draggedView->mapFromGlobal(globalPos);

    return dc->programmaticStartDrag(draggable, globalPos, offset);
}

bool DockWidget::wasRestored() const
{
    return d->m_wasRestored;
}

void DockWidget::setUniqueName(const QString &uniqueName)
{
    d->setUniqueName(uniqueName);
}
