/*
    SPDX-FileCopyrightText: 2018 Michail Vourlakos <mvourlakos@gmail.com>
    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "positioner.h"

// local
#include <coretypes.h>
#include "effects.h"
#include "view.h"
#include "visibilitymanager.h"
#include "../lattecorona.h"
#include "../screenpool.h"
#include "../data/screendata.h"
#include "../layout/centrallayout.h"
#include "../layouts/manager.h"
#include "../settings/universalsettings.h"
#include "../wm/abstractwindowinterface.h"

// Qt
#include <QDebug>

// KDE
#include <KWayland/Client/plasmashell.h>
#include <KWayland/Client/surface.h>
#include <KWindowSystem>

#define RELOCATIONSHOWINGEVENT "viewInRelocationShowing"

namespace Latte {
namespace ViewPart {

Positioner::Positioner(Latte::View *parent)
    : QObject(parent),
      m_view(parent)
{
    m_screenSyncTimer.setSingleShot(true);
    m_screenSyncTimer.setInterval(2000);
    connect(&m_screenSyncTimer, &QTimer::timeout, this, &Positioner::reconsiderScreen);

    //! under X11 it was identified that windows many times especially under screen changes
    //! don't end up at the correct position and size. This timer will enforce repositionings
    //! and resizes every 500ms if the window hasn't end up to correct values and until this
    //! is achieved
    m_validateGeometryTimer.setSingleShot(true);
    m_validateGeometryTimer.setInterval(500);
    connect(&m_validateGeometryTimer, &QTimer::timeout, this, &Positioner::syncGeometry);

    //! syncGeometry() function is costly, so now we make sure that is not executed too often
    m_syncGeometryTimer.setSingleShot(true);
    m_syncGeometryTimer.setInterval(150);
    connect(&m_syncGeometryTimer, &QTimer::timeout, this, &Positioner::immediateSyncGeometry);

    m_corona = qobject_cast<Latte::Corona *>(m_view->corona());

    if (m_corona) {
        if (KWindowSystem::isPlatformX11()) {
            m_trackedWindowId = m_view->winId();
            m_corona->wm()->registerIgnoredWindow(m_trackedWindowId);

            connect(m_view, &Latte::View::forcedShown, this, [&]() {
                m_corona->wm()->unregisterIgnoredWindow(m_trackedWindowId);
                m_trackedWindowId = m_view->winId();
                m_corona->wm()->registerIgnoredWindow(m_trackedWindowId);
            });
        } else {
            connect(m_view, &QWindow::windowTitleChanged, this, &Positioner::updateWaylandId);
            connect(m_corona->wm(), &WindowSystem::AbstractWindowInterface::latteWindowAdded, this, &Positioner::updateWaylandId);
        }

        connect(m_corona->layoutsManager(), &Layouts::Manager::currentLayoutIsSwitching, this, &Positioner::onCurrentLayoutIsSwitching);
        /////

        m_screenSyncTimer.setInterval(qMax(m_corona->universalSettings()->screenTrackerInterval() - 500, 1000));
        connect(m_corona->universalSettings(), &UniversalSettings::screenTrackerIntervalChanged, this, [&]() {
            m_screenSyncTimer.setInterval(qMax(m_corona->universalSettings()->screenTrackerInterval() - 500, 1000));
        });

        connect(m_corona, &Latte::Corona::viewLocationChanged, this, [&]() {
            //! check if an edge has been freed for a primary dock
            //! from another screen
            if (m_view->onPrimary()) {
                m_screenSyncTimer.start();
            }
        });
    }

    init();
}

Positioner::~Positioner()
{
    m_inDelete = true;
    slideOutDuringExit();
    m_corona->wm()->unregisterIgnoredWindow(m_trackedWindowId);

    m_screenSyncTimer.stop();
    m_validateGeometryTimer.stop();
}

void Positioner::init()
{
    //! connections
    connect(this, &Positioner::screenGeometryChanged, this, &Positioner::syncGeometry);

    connect(this, &Positioner::hidingForRelocationStarted, this, &Positioner::updateInRelocationAnimation);
    connect(this, &Positioner::showingAfterRelocationFinished, this, &Positioner::updateInRelocationAnimation);
    connect(this, &Positioner::showingAfterRelocationFinished, this, &Positioner::syncLatteViews);

    connect(m_view, &Latte::View::onPrimaryChanged, this, &Positioner::syncLatteViews);

    connect(this, &Positioner::inSlideAnimationChanged, this, [&]() {
        if (!inSlideAnimation()) {
            syncGeometry();
        }
    });

    connect(this, &Positioner::isStickedOnTopEdgeChanged, this, [&]() {
        if (m_view->formFactor() == Plasma::Types::Vertical) {
            syncGeometry();
        }
    });

    connect(this, &Positioner::isStickedOnBottomEdgeChanged, this, [&]() {
        if (m_view->formFactor() == Plasma::Types::Vertical) {
            syncGeometry();
        }
    });

    connect(m_corona->activitiesConsumer(), &KActivities::Consumer::currentActivityChanged, this, [&]() {
        if (m_view->formFactor() == Plasma::Types::Vertical && m_view->layout() && m_view->layout()->isCurrent()) {
            syncGeometry();
        }
    });

    connect(this, &Positioner::slideOffsetChanged, this, [&]() {
        updatePosition(m_lastAvailableScreenRect);
    });

    connect(m_view, &QQuickWindow::xChanged, this, &Positioner::validateDockGeometry);
    connect(m_view, &QQuickWindow::yChanged, this, &Positioner::validateDockGeometry);
    connect(m_view, &QQuickWindow::widthChanged, this, &Positioner::validateDockGeometry);
    connect(m_view, &QQuickWindow::heightChanged, this, &Positioner::validateDockGeometry);
    connect(m_view, &QQuickWindow::screenChanged, this, &Positioner::currentScreenChanged);
    connect(m_view, &QQuickWindow::screenChanged, this, &Positioner::onScreenChanged);

    connect(m_view, &Latte::View::behaveAsPlasmaPanelChanged, this, &Positioner::syncGeometry);
    connect(m_view, &Latte::View::maxThicknessChanged, this, &Positioner::syncGeometry);

    connect(m_view, &Latte::View::behaveAsPlasmaPanelChanged,  this, [&]() {
        if (!m_view->behaveAsPlasmaPanel() && m_slideOffset != 0) {
            m_slideOffset = 0;
            syncGeometry();
        }
    });

    connect(m_view, &Latte::View::offsetChanged, this, [&]() {
        updatePosition(m_lastAvailableScreenRect);
    });

    connect(m_view, &Latte::View::locationChanged, this, [&]() {
        updateFormFactor();
        syncGeometry();
    });

    connect(m_view, &Latte::View::editThicknessChanged, this, [&]() {
        updateCanvasGeometry(m_lastAvailableScreenRect);
    });

    connect(m_view, &Latte::View::maxLengthChanged, this, [&]() {
        if (m_view->behaveAsPlasmaPanel()) {
            syncGeometry();
        }
    });

    connect(m_view, &Latte::View::normalThicknessChanged, this, [&]() {
        if (m_view->behaveAsPlasmaPanel()) {
            syncGeometry();
        }
    });

    connect(m_view, &Latte::View::screenEdgeMarginEnabledChanged, this, [&]() {
        syncGeometry();
    });

    connect(m_view, &Latte::View::screenEdgeMarginChanged, this, [&]() {
        syncGeometry();
    });

    connect(m_view, &View::layoutChanged, this, [&]() {
        if (m_nextLayoutName.isEmpty() && m_view->layout() && m_view->formFactor() == Plasma::Types::Vertical) {
            syncGeometry();
        }
    });

    connect(m_view->effects(), &Latte::ViewPart::Effects::drawShadowsChanged, this, [&]() {
        if (!m_view->behaveAsPlasmaPanel()) {
            syncGeometry();
        }
    });

    connect(m_view->effects(), &Latte::ViewPart::Effects::innerShadowChanged, this, [&]() {
        if (m_view->behaveAsPlasmaPanel()) {
            syncGeometry();
        }
    });

    connect(qGuiApp, &QGuiApplication::screenAdded, this, &Positioner::onScreenChanged);
    connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &Positioner::onScreenChanged);

    connect(m_view, &Latte::View::visibilityChanged, this, &Positioner::initDelayedSignals);

    initSignalingForLocationChangeSliding();
}

void Positioner::initDelayedSignals()
{
    connect(m_view->visibility(), &ViewPart::VisibilityManager::isHiddenChanged, this, [&]() {
        if (m_view->behaveAsPlasmaPanel() && !m_view->visibility()->isHidden() && qAbs(m_slideOffset)>0) {
            //! ignore any checks to make sure the panel geometry is up-to-date
            immediateSyncGeometry();
        }
    });
}

void Positioner::updateWaylandId()
{
    QString validTitle = m_view->validTitle();
    if (validTitle.isEmpty()) {
        return;
    }

    Latte::WindowSystem::WindowId newId = m_corona->wm()->winIdFor("latte-dock", validTitle);

    if (m_trackedWindowId != newId) {
        if (!m_trackedWindowId.isNull()) {
            m_corona->wm()->unregisterIgnoredWindow(m_trackedWindowId);
        }

        m_trackedWindowId = newId;
        m_corona->wm()->registerIgnoredWindow(m_trackedWindowId);

        emit winIdChanged();
    }
}

bool Positioner::inRelocationShowing() const
{
    return m_inRelocationShowing;
}

void Positioner::setInRelocationShowing(bool active)
{
    if (m_inRelocationShowing == active) {
        return;
    }

    m_inRelocationShowing = active;

    if (m_inRelocationShowing) {
        m_view->visibility()->addBlockHidingEvent(RELOCATIONSHOWINGEVENT);
    } else {
        m_view->visibility()->removeBlockHidingEvent(RELOCATIONSHOWINGEVENT);
    }
}

int Positioner::currentScreenId() const
{
    auto *latteCorona = qobject_cast<Latte::Corona *>(m_view->corona());

    if (latteCorona) {
        return latteCorona->screenPool()->id(m_screenNameToFollow);
    }

    return -1;
}

Latte::WindowSystem::WindowId Positioner::trackedWindowId()
{
    if (KWindowSystem::isPlatformWayland() && m_trackedWindowId.toInt() <= 0) {
        updateWaylandId();
    }

    return m_trackedWindowId;
}

QString Positioner::currentScreenName() const
{
    return m_screenNameToFollow;
}

WindowSystem::AbstractWindowInterface::Slide Positioner::slideLocation(Plasma::Types::Location location)
{
    auto slideedge = WindowSystem::AbstractWindowInterface::Slide::None;

    if (location == Plasma::Types::Floating && m_view->containment()) {
        location = m_view->containment()->location();
    }

    switch (location) {
    case Plasma::Types::TopEdge:
        slideedge = WindowSystem::AbstractWindowInterface::Slide::Top;
        break;

    case Plasma::Types::RightEdge:
        slideedge = WindowSystem::AbstractWindowInterface::Slide::Right;
        break;

    case Plasma::Types::BottomEdge:
        slideedge = WindowSystem::AbstractWindowInterface::Slide::Bottom;
        break;

    case Plasma::Types::LeftEdge:
        slideedge = WindowSystem::AbstractWindowInterface::Slide::Left;
        break;

    default:
        qDebug() << staticMetaObject.className() << "wrong location";
        break;
    }

    return slideedge;
}

void Positioner::slideOutDuringExit(Plasma::Types::Location location)
{
    if (m_view->isVisible()) {
        m_corona->wm()->slideWindow(*m_view, slideLocation(location));
        m_view->setVisible(false);
    }
}

void Positioner::slideInDuringStartup()
{
    m_corona->wm()->slideWindow(*m_view, slideLocation(m_view->containment()->location()));
}

void Positioner::onCurrentLayoutIsSwitching(const QString &layoutName)
{
    if (!m_view || !m_view->layout() || m_view->layout()->name() != layoutName || !m_view->isVisible()) {
        return;
    }

    m_inLayoutUnloading = true;
    slideOutDuringExit();
}

void Positioner::setWindowOnActivities(const Latte::WindowSystem::WindowId &wid, const QStringList &activities)
{
    m_corona->wm()->setWindowOnActivities(wid, activities);
}

void Positioner::syncLatteViews()
{
    if (m_view->layout()) {
        //! This is needed in case the edge there are views that must be deleted
        //! after screen edges changes
        m_view->layout()->syncLatteViewsToScreens();
    }
}

void Positioner::updateContainmentScreen()
{
    if (m_view->containment()) {
        m_view->containment()->reactToScreenChange();
    }
}

//! this function updates the dock's associated screen.
//! updateScreenId = true, update also the m_screenNameToFollow
//! updateScreenId = false, do not update the m_screenNameToFollow
//! that way an explicit dock can be shown in another screen when
//! there isnt a tasks dock running in the system and for that
//! dock its first origin screen is stored and that way when
//! that screen is reconnected the dock will return to its original
//! place
void Positioner::setScreenToFollow(QScreen *scr, bool updateScreenId)
{
    if (!scr || (scr && (m_screenToFollow == scr) && (m_view->screen() == scr))) {
        return;
    }

    qDebug() << "setScreenToFollow() called for screen:" << scr->name() << " update:" << updateScreenId;

    m_screenToFollow = scr;

    if (updateScreenId) {
        m_screenNameToFollow = scr->name();
    }

    qDebug() << "adapting to screen...";
    m_view->setScreen(scr);

    updateContainmentScreen();

    connect(scr, &QScreen::geometryChanged, this, &Positioner::screenGeometryChanged);
    syncGeometry();
    m_view->updateAbsoluteGeometry(true);
    qDebug() << "setScreenToFollow() ended...";

    emit screenGeometryChanged();
    emit currentScreenChanged();
}

//! the main function which decides if this dock is at the
//! correct screen
void Positioner::reconsiderScreen()
{
    if (m_inDelete) {
        return;
    }

    qDebug() << "reconsiderScreen() called...";
    qDebug() << "  Delayer  ";

    for (const auto scr : qGuiApp->screens()) {
        qDebug() << "      D, found screen: " << scr->name();
    }

    bool screenExists{false};

    //!check if the associated screen is running
    for (const auto scr : qGuiApp->screens()) {
        if (m_screenNameToFollow == scr->name()
                || (m_view->onPrimary() && scr == qGuiApp->primaryScreen())) {
            screenExists = true;
        }
    }

    qDebug() << "dock screen exists  ::: " << screenExists;

    //! 1.a primary dock must be always on the primary screen
    if (m_view->onPrimary() && (m_screenNameToFollow != qGuiApp->primaryScreen()->name()
                                || m_screenToFollow != qGuiApp->primaryScreen()
                                || m_view->screen() != qGuiApp->primaryScreen())) {
        //! case 1
        qDebug() << "reached case 1: of updating dock primary screen...";
        setScreenToFollow(qGuiApp->primaryScreen());
    } else if (!m_view->onPrimary()) {
        //! 2.an explicit dock must be always on the correct associated screen
        //! there are cases that window manager misplaces the dock, this function
        //! ensures that this dock will return at its correct screen
        for (const auto scr : qGuiApp->screens()) {
            if (scr && scr->name() == m_screenNameToFollow) {
                qDebug() << "reached case 2: updating the explicit screen for dock...";
                setScreenToFollow(scr);
                break;
            }
        }
    }

    syncGeometry();
    qDebug() << "reconsiderScreen() ended...";
}

void Positioner::onScreenChanged(QScreen *scr)
{
    m_screenSyncTimer.start();

    //! this is needed in order to update the struts on screen change
    //! and even though the geometry has been set correctly the offsets
    //! of the screen must be updated to the new ones
    if (m_view->visibility() && m_view->visibility()->mode() == Latte::Types::AlwaysVisible) {
        m_view->updateAbsoluteGeometry(true);
    }
}

void Positioner::syncGeometry()
{
    if (!(m_view->screen() && m_view->containment()) || m_inDelete || m_slideOffset!=0 || inSlideAnimation()) {
        return;
    }

    qDebug() << "syncGeometry() called...";

    if (!m_syncGeometryTimer.isActive()) {
        m_syncGeometryTimer.start();
    }
}

void Positioner::immediateSyncGeometry()
{
    bool found{false};

    qDebug() << "immediateSyncGeometry() called...";

    //! before updating the positioning and geometry of the dock
    //! we make sure that the dock is at the correct screen
    if (m_view->screen() != m_screenToFollow) {
        qDebug() << "Sync Geometry screens inconsistent!!!! ";

        if (m_screenToFollow) {
            qDebug() << "Sync Geometry screens inconsistent for m_screenToFollow:" << m_screenToFollow->name() << " dock screen:" << m_view->screen()->name();
        }

        if (!m_screenSyncTimer.isActive()) {
            m_screenSyncTimer.start();
        }
    } else {
        found = true;
    }

    //! if the dock isnt at the correct screen the calculations
    //! are not executed
    if (found) {
        //! compute the free screen rectangle for vertical panels only once
        //! this way the costly QRegion computations are calculated only once
        //! instead of two times (both inside the resizeWindow and the updatePosition)
        QRegion freeRegion;;
        QRect maximumRect;
        QRect availableScreenRect{m_view->screen()->geometry()};

        if (m_view->formFactor() == Plasma::Types::Vertical) {
            QString layoutName = m_view->layout() ? m_view->layout()->name() : QString();
            auto latteCorona = qobject_cast<Latte::Corona *>(m_view->corona());
            int fixedScreen = m_view->onPrimary() ? latteCorona->screenPool()->primaryScreenId() : m_view->containment()->screen();

            QList<Types::Visibility> ignoreModes({Latte::Types::AutoHide,
                                                  Latte::Types::SidebarOnDemand,
                                                  Latte::Types::SidebarAutoHide});

            QList<Plasma::Types::Location> ignoreEdges({Plasma::Types::LeftEdge,
                                                        Plasma::Types::RightEdge});

            if (m_isStickedOnTopEdge && m_isStickedOnBottomEdge) {
                //! dont send an empty edges array because that means include all screen edges in calculations
                ignoreEdges << Plasma::Types::TopEdge;
                ignoreEdges << Plasma::Types::BottomEdge;
            } else {
                if (m_isStickedOnTopEdge) {
                    ignoreEdges << Plasma::Types::TopEdge;
                }

                if (m_isStickedOnBottomEdge) {
                    ignoreEdges << Plasma::Types::BottomEdge;
                }
            }

            QString activityid = m_view->layout() ? m_view->layout()->lastUsedActivity() : QString();
            freeRegion = latteCorona->availableScreenRegionWithCriteria(fixedScreen, activityid, ignoreModes, ignoreEdges);

            maximumRect = maximumNormalGeometry();
            QRegion availableRegion = freeRegion.intersected(maximumRect);

            availableScreenRect = freeRegion.intersected(maximumRect).boundingRect();
            float area = 0;

            //! it is used to choose which or the availableRegion rectangles will
            //! be the one representing dock geometry
            for (QRegion::const_iterator p_rect=availableRegion.begin(); p_rect!=availableRegion.end(); ++p_rect) {
                //! the area of each rectangle in calculated in squares of 50x50
                //! this is a way to avoid enourmous numbers for area value
                float tempArea = (float)((*p_rect).width() * (*p_rect).height()) / 2500;

                if (tempArea > area) {
                    availableScreenRect = (*p_rect);
                    area = tempArea;
                }
            }

            validateTopBottomBorders(availableScreenRect, freeRegion);

            m_lastAvailableScreenRegion = freeRegion;
        } else {
            m_view->effects()->setForceTopBorder(false);
            m_view->effects()->setForceBottomBorder(false);
        }

        m_lastAvailableScreenRect = availableScreenRect;

        m_view->effects()->updateEnabledBorders();

        resizeWindow(availableScreenRect);
        updatePosition(availableScreenRect);
        updateCanvasGeometry(availableScreenRect);

        qDebug() << "syncGeometry() calculations for screen: " << m_view->screen()->name() << " _ " << m_view->screen()->geometry();
        qDebug() << "syncGeometry() calculations for edge: " << m_view->location();
    }

    qDebug() << "syncGeometry() ended...";

    // qDebug() << "dock geometry:" << qRectToStr(geometry());
}

void Positioner::validateDockGeometry()
{
    if (m_slideOffset==0 && m_view->geometry() != m_validGeometry) {
        m_validateGeometryTimer.start();
    }
}

QRect Positioner::canvasGeometry()
{
    return m_canvasGeometry;
}

void Positioner::setCanvasGeometry(const QRect &geometry)
{
    if (m_canvasGeometry == geometry) {
        return;
    }

    m_canvasGeometry = geometry;
    emit canvasGeometryChanged();
}


//! this is used mainly from vertical panels in order to
//! to get the maximum geometry that can be used from the dock
//! based on their alignment type and the location dock
QRect Positioner::maximumNormalGeometry()
{
    int xPos = 0;
    int yPos = m_view->screen()->geometry().y();;
    int maxHeight = m_view->screen()->geometry().height();
    int maxWidth = m_view->maxNormalThickness();
    QRect maxGeometry;
    maxGeometry.setRect(0, 0, maxWidth, maxHeight);

    switch (m_view->location()) {
    case Plasma::Types::LeftEdge:
        xPos = m_view->screen()->geometry().x();
        maxGeometry.setRect(xPos, yPos, maxWidth, maxHeight);
        break;

    case Plasma::Types::RightEdge:
        xPos = m_view->screen()->geometry().right() - maxWidth + 1;
        maxGeometry.setRect(xPos, yPos, maxWidth, maxHeight);
        break;

    default:
        //! bypass clang warnings
        break;
    }

    return maxGeometry;
}

void Positioner::validateTopBottomBorders(QRect availableScreenRect, QRegion availableScreenRegion)
{
    //! Check if the the top/bottom borders must be drawn also
    int edgeMargin = qMax(1, m_view->screenEdgeMargin());

    if (availableScreenRect.top() != m_view->screenGeometry().top()) {
        //! check top border
        QRegion fitInRegion = QRect(m_view->screenGeometry().x(), availableScreenRect.y()-1, edgeMargin, 1);
        QRegion subtracted = fitInRegion.subtracted(availableScreenRegion);

        if (subtracted.isNull()) {
            //!FitIn rectangle fits TOTALLY in the free screen region and as such
            //!the top border should be drawn
            m_view->effects()->setForceTopBorder(true);
        } else {
            m_view->effects()->setForceTopBorder(false);
        }
    } else {
        m_view->effects()->setForceTopBorder(false);
    }

    if (availableScreenRect.bottom() != m_view->screenGeometry().bottom()) {
        //! check top border
        QRegion fitInRegion = QRect(m_view->screenGeometry().x(), availableScreenRect.bottom()+1, edgeMargin, 1);
        QRegion subtracted = fitInRegion.subtracted(availableScreenRegion);

        if (subtracted.isNull()) {
            //!FitIn rectangle fits TOTALLY in the free screen region and as such
            //!the BOTTOM border should be drawn
            m_view->effects()->setForceBottomBorder(true);
        } else {
            m_view->effects()->setForceBottomBorder(false);
        }
    } else {
        m_view->effects()->setForceBottomBorder(false);
    }
}

void Positioner::updateCanvasGeometry(QRect availableScreenRect)
{
    if (availableScreenRect.isEmpty()) {
        return;
    }

    QRect canvas;
    QRect screenGeometry{m_view->screen()->geometry()};
    int thickness{m_view->editThickness()};

    if (m_view->formFactor() == Plasma::Types::Vertical) {
        canvas.setWidth(thickness);
        canvas.setHeight(availableScreenRect.height());
    } else {
        canvas.setWidth(screenGeometry.width());
        canvas.setHeight(thickness);
    }

    switch (m_view->location()) {
    case Plasma::Types::TopEdge:
        canvas.moveLeft(screenGeometry.x());
        canvas.moveTop(screenGeometry.y());
        break;

    case Plasma::Types::BottomEdge:
        canvas.moveLeft(screenGeometry.x());
        canvas.moveTop(screenGeometry.bottom() - thickness + 1);
        break;

    case Plasma::Types::RightEdge:
        canvas.moveLeft(screenGeometry.right() - thickness + 1);
        canvas.moveTop(availableScreenRect.y());
        break;

    case Plasma::Types::LeftEdge:
        canvas.moveLeft(availableScreenRect.x());
        canvas.moveTop(availableScreenRect.y());
        break;

    default:
        qWarning() << "wrong location, couldn't update the canvas config window geometry " << m_view->location();
    }

    setCanvasGeometry(canvas);
}

void Positioner::updatePosition(QRect availableScreenRect)
{
    QRect screenGeometry{availableScreenRect};
    QPoint position;
    position = {0, 0};

    const auto gap = [&](int scr_length) -> int {
        return static_cast<int>(scr_length * m_view->offset());
    };
    const auto gapCentered = [&](int scr_length) -> int {
        return static_cast<int>(scr_length * ((1 - m_view->maxLength()) / 2) + scr_length * m_view->offset());
    };
    const auto gapReversed = [&](int scr_length) -> int {
        return static_cast<int>(scr_length - (scr_length * m_view->maxLength()) - gap(scr_length));
    };

    int cleanThickness = m_view->normalThickness() - m_view->effects()->innerShadow();

    int screenEdgeMargin = m_view->behaveAsPlasmaPanel() ? m_view->screenEdgeMargin() - qAbs(m_slideOffset) : 0;

    switch (m_view->location()) {
    case Plasma::Types::TopEdge:
        if (m_view->behaveAsPlasmaPanel()) {
            int y = screenGeometry.y() + screenEdgeMargin;

            if (m_view->alignment() == Latte::Types::Left) {
                position = {screenGeometry.x() + gap(screenGeometry.width()), y};
            } else if (m_view->alignment() == Latte::Types::Right) {
                position = {screenGeometry.x() + gapReversed(screenGeometry.width()) + 1, y};
            } else {
                position = {screenGeometry.x() + gapCentered(screenGeometry.width()), y};
            }
        } else {
            position = {screenGeometry.x(), screenGeometry.y()};
        }

        break;

    case Plasma::Types::BottomEdge:
        if (m_view->behaveAsPlasmaPanel()) {
            int y = screenGeometry.y() + screenGeometry.height() - cleanThickness - screenEdgeMargin;

            if (m_view->alignment() == Latte::Types::Left) {
                position = {screenGeometry.x() + gap(screenGeometry.width()), y};
            } else if (m_view->alignment() == Latte::Types::Right) {
                position = {screenGeometry.x() + gapReversed(screenGeometry.width()) + 1, y};
            } else {
                position = {screenGeometry.x() + gapCentered(screenGeometry.width()), y};
            }
        } else {
            position = {screenGeometry.x(), screenGeometry.y() + screenGeometry.height() - m_view->height()};
        }

        break;

    case Plasma::Types::RightEdge:
        if (m_view->behaveAsPlasmaPanel()) {
            int x = availableScreenRect.right() - cleanThickness + 1 - screenEdgeMargin;

            if (m_view->alignment() == Latte::Types::Top) {
                position = {x, availableScreenRect.y() + gap(availableScreenRect.height())};
            } else if (m_view->alignment() == Latte::Types::Bottom) {
                position = {x, availableScreenRect.y() + gapReversed(availableScreenRect.height()) + 1};
            } else {
                position = {x, availableScreenRect.y() + gapCentered(availableScreenRect.height())};
            }
        } else {
            position = {availableScreenRect.right() - m_view->width() + 1, availableScreenRect.y()};
        }

        break;

    case Plasma::Types::LeftEdge:
        if (m_view->behaveAsPlasmaPanel()) {
            int x = availableScreenRect.x() + screenEdgeMargin;

            if (m_view->alignment() == Latte::Types::Top) {
                position = {x, availableScreenRect.y() + gap(availableScreenRect.height())};
            } else if (m_view->alignment() == Latte::Types::Bottom) {
                position = {x, availableScreenRect.y() + gapReversed(availableScreenRect.height()) + 1};
            } else {
                position = {x, availableScreenRect.y() + gapCentered(availableScreenRect.height())};
            }
        } else {
            position = {availableScreenRect.x(), availableScreenRect.y()};
        }

        break;

    default:
        qWarning() << "wrong location, couldn't update the panel position"
                   << m_view->location();
    }

    if (m_slideOffset == 0 || m_nextScreenEdge != Plasma::Types::Floating /*exactly after relocating and changing screen edge*/) {
        //! update valid geometry in normal positioning
        m_validGeometry.moveTopLeft(position);
    } else {
        //! when sliding in/out update only the relevant axis for the screen_edge in
        //! to not mess the calculations and the automatic geometry checkers that
        //! View::Positioner is using.
        if (m_view->formFactor() == Plasma::Types::Horizontal) {
            m_validGeometry.moveLeft(position.x());
        } else {
            m_validGeometry.moveTop(position.y());
        }
    }

    m_view->setPosition(position);

    if (m_view->surface()) {
        m_view->surface()->setPosition(position);
    }
}

int Positioner::slideOffset() const
{
    return m_slideOffset;
}

void Positioner::setSlideOffset(int offset)
{
    if (m_slideOffset == offset) {
        return;
    }

    m_slideOffset = offset;
    emit slideOffsetChanged();
}


void Positioner::resizeWindow(QRect availableScreenRect)
{
    QSize screenSize = m_view->screen()->size();
    QSize size = (m_view->formFactor() == Plasma::Types::Vertical) ? QSize(m_view->maxThickness(), availableScreenRect.height()) : QSize(screenSize.width(), m_view->maxThickness());

    if (m_view->formFactor() == Plasma::Types::Vertical) {
        //qDebug() << "MAXIMUM RECT :: " << maximumRect << " - AVAILABLE RECT :: " << availableRect;
        if (m_view->behaveAsPlasmaPanel()) {
            size.setWidth(m_view->normalThickness());
            size.setHeight(static_cast<int>(m_view->maxLength() * availableScreenRect.height()));
        }
    } else {
        if (m_view->behaveAsPlasmaPanel()) {
            size.setWidth(static_cast<int>(m_view->maxLength() * screenSize.width()));
            size.setHeight(m_view->normalThickness());
        }
    }

    m_validGeometry.setSize(size);

    m_view->setMinimumSize(size);
    m_view->setMaximumSize(size);
    m_view->resize(size);

    if (m_view->formFactor() == Plasma::Types::Horizontal) {
        emit windowSizeChanged();
    }
}

void Positioner::updateFormFactor()
{
    if (!m_view->containment())
        return;

    switch (m_view->location()) {
    case Plasma::Types::TopEdge:
    case Plasma::Types::BottomEdge:
        m_view->containment()->setFormFactor(Plasma::Types::Horizontal);
        break;

    case Plasma::Types::LeftEdge:
    case Plasma::Types::RightEdge:
        m_view->containment()->setFormFactor(Plasma::Types::Vertical);
        break;

    default:
        qWarning() << "wrong location, couldn't update the panel position" << m_view->location();
    }
}

void Positioner::onLastRepositionApplyEvent()
{
    m_view->effects()->setAnimationsBlocked(false);
    setInRelocationShowing(true);
    emit showingAfterRelocationFinished();
    emit edgeChanged();

    if (m_repositionFromViewSettingsWindow) {
        m_repositionFromViewSettingsWindow = false;
        m_view->showSettingsWindow();
    }
}

void Positioner::initSignalingForLocationChangeSliding()
{
    connect(this, &Positioner::hidingForRelocationStarted, this, &Positioner::onHideWindowsForSlidingOut);

    //! SCREEN_EDGE
    connect(m_view, &View::locationChanged, this, [&]() {
        if (m_nextScreenEdge != Plasma::Types::Floating) {
            bool isrelocationlastevent = isLastHidingRelocationEvent();
            immediateSyncGeometry();
            m_nextScreenEdge = Plasma::Types::Floating;

            //! make sure that View has been repositioned properly in next screen edge and show view afterwards
            if (isrelocationlastevent) {
                QTimer::singleShot(100, [this]() {
                    onLastRepositionApplyEvent();
                });
            }
        }
    });

    //! SCREEN
    connect(m_view, &QQuickView::screenChanged, this, [&]() {
        if (m_nextScreen
                && m_nextScreen == m_view->screen()
                && m_nextScreen->geometry().contains(m_view->geometry().center())) {

            bool isrelocationlastevent = isLastHidingRelocationEvent();
            m_nextScreen = nullptr;
            m_nextScreenName = "";

            //! make sure that View has been repositioned properly in next screen and show view afterwards
            if (isrelocationlastevent) {
                QTimer::singleShot(100, [this]() {
                    onLastRepositionApplyEvent();
                });
            }
        }
    });

    //! LAYOUT
    connect(m_view, &View::layoutChanged, this, [&]() {
        if (!m_nextLayoutName.isEmpty() && m_view->layout()) {
            bool isrelocationlastevent = isLastHidingRelocationEvent();
            m_nextLayoutName = "";

            //! make sure that View has been repositioned properly in next layout and show view afterwards
            if (isrelocationlastevent) {
                QTimer::singleShot(100, [this]() {
                    onLastRepositionApplyEvent();
                });
            }
        }
    });

    //! APPLY CHANGES
    connect(this, &Positioner::hidingForRelocationFinished, this, [&]() {
        //! must be called only if relocation is animated
        if (m_repositionIsAnimated) {
            m_repositionIsAnimated = false;
            m_view->effects()->setAnimationsBlocked(true);
        }

        //! LAYOUT
        if (!m_nextLayoutName.isEmpty()) {
            m_corona->layoutsManager()->moveView(m_view->layout()->name(), m_view->containment()->id(), m_nextLayoutName);
        }

        //! SCREEN
        if (!m_nextScreenName.isEmpty()) {
            bool nextonprimary = (m_nextScreenName == Latte::Data::Screen::ONPRIMARYNAME);
            m_nextScreen = qGuiApp->primaryScreen();

            if (!nextonprimary) {
                for (const auto scr : qGuiApp->screens()) {
                    if (scr && scr->name() == m_nextScreenName) {
                        m_nextScreen = scr;
                        break;
                    }
                }
            }

            m_view->setOnPrimary(nextonprimary);
            setScreenToFollow(m_nextScreen);
        }

        //! SCREEN_EDGE
        if (m_nextScreenEdge != Plasma::Types::Floating) {
            m_view->setLocation(m_nextScreenEdge);
        }

        //! ALIGNMENT
        if (m_nextAlignment != Latte::Types::NoneAlignment && m_nextAlignment != m_view->alignment()) {
            m_view->setAlignment(m_nextAlignment);
            m_nextAlignment = Latte::Types::NoneAlignment;
        }
    });
}

bool Positioner::inLayoutUnloading()
{
    return m_inLayoutUnloading;
}

bool Positioner::inRelocationAnimation()
{
    return ((m_nextScreenEdge != Plasma::Types::Floating) || !m_nextLayoutName.isEmpty() || !m_nextScreenName.isEmpty());
}

bool Positioner::inSlideAnimation() const
{
    return m_inSlideAnimation;
}

void Positioner::setInSlideAnimation(bool active)
{
    if (m_inSlideAnimation == active) {
        return;
    }

    m_inSlideAnimation = active;
    emit inSlideAnimationChanged();
}

bool Positioner::isCursorInsideView() const
{
    return m_view->geometry().contains(QCursor::pos(m_screenToFollow));
}

bool Positioner::isStickedOnTopEdge() const
{
    return m_isStickedOnTopEdge;
}

void Positioner::setIsStickedOnTopEdge(bool sticked)
{
    if (m_isStickedOnTopEdge == sticked) {
        return;
    }

    m_isStickedOnTopEdge = sticked;
    emit isStickedOnTopEdgeChanged();
}

bool Positioner::isStickedOnBottomEdge() const
{
    return m_isStickedOnBottomEdge;
}

void Positioner::setIsStickedOnBottomEdge(bool sticked)
{
    if (m_isStickedOnBottomEdge == sticked) {
        return;
    }

    m_isStickedOnBottomEdge = sticked;
    emit isStickedOnBottomEdgeChanged();
}

void Positioner::updateInRelocationAnimation()
{
    bool inrelocationanimation = inRelocationAnimation();

    if (m_inRelocationAnimation == inrelocationanimation) {
        return;
    }

    m_inRelocationAnimation = inrelocationanimation;
    emit inRelocationAnimationChanged();
}

bool Positioner::isLastHidingRelocationEvent() const
{
    int events{0};

    if (!m_nextLayoutName.isEmpty()) {
        events++;
    }

    if (!m_nextScreenName.isEmpty()){
        events++;
    }

    if (m_nextScreenEdge != Plasma::Types::Floating) {
        events++;
    }

    return (events <= 1);
}

void Positioner::setNextLocation(const QString layoutName, const QString screenName, int edge, int alignment)
{
    bool isanimated{false};
    bool haschanges{false};

    //! LAYOUT
    if (!layoutName.isEmpty()) {
        auto layout = m_view->layout();
        auto origin = qobject_cast<CentralLayout *>(layout);
        auto destination = m_corona->layoutsManager()->synchronizer()->centralLayout(layoutName);

        if (origin && destination && origin!=destination) {
            //! Needs to be updated; when the next layout is in the same Visible Workarea
            //! with the old one changing layouts should be instant
            bool inVisibleWorkarea{origin->lastUsedActivity() == destination->lastUsedActivity()};

            haschanges = true;
            m_nextLayoutName = layoutName;

            if (!inVisibleWorkarea) {
                isanimated = true;
            }
        }
    }

    //! SCREEN
    if (!screenName.isEmpty()) {
        bool nextonprimary = (screenName == Latte::Data::Screen::ONPRIMARYNAME);

        if ( (m_view->onPrimary() && !nextonprimary) /*primary -> explicit*/
             || (!m_view->onPrimary() && nextonprimary) /*explicit -> primary*/
             || (!m_view->onPrimary() && !nextonprimary && screenName != currentScreenName()) ) { /*explicit -> new_explicit*/

            QString nextscreenname = nextonprimary ? qGuiApp->primaryScreen()->name() : screenName;

            if (currentScreenName() == nextscreenname) {
                m_view->setOnPrimary(nextonprimary);
                updateContainmentScreen();
            } else {
                m_nextScreenName = screenName;
                isanimated = true;
                haschanges = true;
            }
        }
    }

    //! SCREEN_EDGE
    if (edge != Plasma::Types::Floating) {
        if (edge != m_view->location()) {
            m_nextScreenEdge = static_cast<Plasma::Types::Location>(edge);
            isanimated = true;
            haschanges = true;
        }
    }

    //! ALIGNMENT
    if (alignment != Latte::Types::NoneAlignment && m_view->alignment() != alignment) {
        m_nextAlignment = static_cast<Latte::Types::Alignment>(alignment);
        haschanges = true;
    }

    m_repositionIsAnimated = isanimated;
    m_repositionFromViewSettingsWindow = m_view->settingsWindowIsShown();

    if (isanimated) {
        emit hidingForRelocationStarted();
    } else if (haschanges){
        emit hidingForRelocationFinished();
    }
}

}
}
