/* === This file is part of Calamares - <https://calamares.io> ===
 *
 *   SPDX-FileCopyrightText: 2014-2016 Teo Mrnjavac <teo@kde.org>
 *   SPDX-License-Identifier: GPL-3.0-or-later
 *
 *   Calamares is Free Software: see the License-Identifier above.
 *
 */

#include "PartitionSplitterWidget.h"

#include "core/ColorUtils.h"
#include "core/KPMHelpers.h"

#include "partition/PartitionIterator.h"
#include "partition/PartitionQuery.h"
#include "utils/Logger.h"

#include "utils/Gui.h"

#include <kpmcore/core/device.h>
#include <kpmcore/core/partition.h>

#include <QApplication>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QStyleOption>

using Calamares::Partition::PartitionIterator;

static const int VIEW_HEIGHT = qMax( Calamares::defaultFontHeight() + 8,  // wins out with big fonts
                                     int( Calamares::defaultFontHeight() * 0.6 ) + 22 );  // wins out with small fonts
static const int CORNER_RADIUS = 3;
static const int EXTENDED_PARTITION_MARGIN = qMax( 4, VIEW_HEIGHT / 6 );

/** @brief Applies @p operation to each item
 *
 * A PartitionSplitterItem can contain a tree of items (each item has its
 * own list of children) so recurse over all the children. Returns a count
 * of how many items were affected.
 */
static int
countTransform( QVector< PartitionSplitterItem >& items,
                const std::function< bool( PartitionSplitterItem& ) >& operation )
{
    int opCount = 0;
    for ( auto it = items.begin(); it != items.end(); ++it )
    {
        if ( operation( *it ) )
        {
            opCount++;
        }

        opCount += countTransform( it->children, operation );
    }
    return opCount;
}

static PartitionSplitterItem
findTransform( QVector< PartitionSplitterItem >& items, std::function< bool( PartitionSplitterItem& ) > condition )
{
    for ( auto it = items.begin(); it != items.end(); ++it )
    {
        if ( condition( *it ) )
        {
            return *it;
        }

        PartitionSplitterItem candidate = findTransform( it->children, condition );
        if ( !candidate.isNull() )
        {
            return candidate;
        }
    }
    return PartitionSplitterItem::null();
}

PartitionSplitterWidget::PartitionSplitterWidget( QWidget* parent )
    : QWidget( parent )
    , m_itemToResize( PartitionSplitterItem::null() )
    , m_itemToResizeNext( PartitionSplitterItem::null() )
    , m_itemMinSize( 0 )
    , m_itemMaxSize( 0 )
    , m_itemPrefSize( 0 )
    , m_resizing( false )
    , m_resizeHandleX( 0 )
    , HANDLE_SNAP( QApplication::startDragDistance() )
    , m_drawNestedPartitions( false )
{
    setMouseTracking( true );
}

void
PartitionSplitterWidget::init( Device* dev, bool drawNestedPartitions )
{
    m_drawNestedPartitions = drawNestedPartitions;
    QVector< PartitionSplitterItem > allPartitionItems;
    PartitionSplitterItem* extendedPartitionItem = nullptr;
    for ( auto it = PartitionIterator::begin( dev ); it != PartitionIterator::end( dev ); ++it )
    {
        PartitionSplitterItem newItem = { ( *it )->partitionPath(),
                                          ColorUtils::colorForPartition( *it ),
                                          Calamares::Partition::isPartitionFreeSpace( *it ),
                                          ( *it )->capacity(),
                                          PartitionSplitterItem::Normal,
                                          {} };

        // If we don't draw child partitions of a partitions as child partitions, we
        // need to flatten the items tree into an items list
        if ( drawNestedPartitions )
        {
            if ( ( *it )->roles().has( PartitionRole::Logical ) && extendedPartitionItem )
            {
                extendedPartitionItem->children.append( newItem );
            }
            else
            {
                allPartitionItems.append( newItem );
                if ( ( *it )->roles().has( PartitionRole::Extended ) )
                {
                    extendedPartitionItem = &allPartitionItems.last();
                }
            }
        }
        else
        {
            if ( !( *it )->roles().has( PartitionRole::Extended ) )
            {
                allPartitionItems.append( newItem );
            }
        }
    }

    setupItems( allPartitionItems );
}

void
PartitionSplitterWidget::setupItems( const QVector< PartitionSplitterItem >& items )
{
    m_itemToResize = PartitionSplitterItem::null();
    m_itemToResizeNext = PartitionSplitterItem::null();
    m_itemToResizePath.clear();

    m_items.clear();
    m_items = items;
    repaint();
    for ( const PartitionSplitterItem& item : items )
    {
        cDebug() << "PSI added item" << item.itemPath << "size" << item.size;
    }
}

void
PartitionSplitterWidget::setSplitPartition( const QString& path, qint64 minSize, qint64 maxSize, qint64 preferredSize )
{
    cDebug() << "path:" << path << Logger::Continuation << "minSize:" << minSize << Logger::Continuation
             << "maxSize:" << maxSize << Logger::Continuation << "prfSize:" << preferredSize;

    if ( m_itemToResize && m_itemToResizeNext )
    {
        cDebug() << "NOTICE: trying to split partition but partition to split is already set.";

        // We need to remove the itemToResizeNext from wherever it is
        for ( int i = 0; i < m_items.count(); ++i )
        {
            if ( m_items[ i ].itemPath == m_itemToResize.itemPath
                 && m_items[ i ].status == PartitionSplitterItem::Resizing && i + 1 < m_items.count() )
            {
                m_items[ i ].size = m_items[ i ].size + m_itemToResizeNext.size;
                m_items[ i ].status = PartitionSplitterItem::Normal;
                m_items.removeAt( i + 1 );
                m_itemToResizeNext = PartitionSplitterItem::null();
                break;
            }
            else if ( !m_items[ i ].children.isEmpty() )
            {
                for ( int j = 0; j < m_items[ i ].children.count(); ++j )
                {
                    if ( m_items[ i ].children[ j ].itemPath == m_itemToResize.itemPath
                         && j + 1 < m_items[ i ].children.count() )
                    {
                        m_items[ i ].children[ j ].size = m_items[ i ].children[ j ].size + m_itemToResizeNext.size;
                        m_items[ i ].children[ j ].status = PartitionSplitterItem::Normal;
                        m_items[ i ].children.removeAt( j + 1 );
                        m_itemToResizeNext = PartitionSplitterItem::null();
                        break;
                    }
                }
                if ( m_itemToResizeNext.isNull() )
                {
                    break;
                }
            }
        }

        m_itemToResize = PartitionSplitterItem::null();
        m_itemToResizePath.clear();
    }

    PartitionSplitterItem itemToResize = findTransform( m_items,
                                                        [ path ]( PartitionSplitterItem& item ) -> bool
                                                        {
                                                            if ( path == item.itemPath )
                                                            {
                                                                item.status = PartitionSplitterItem::Resizing;
                                                                return true;
                                                            }
                                                            return false;
                                                        } );

    if ( itemToResize.isNull() )
    {
        return;
    }
    cDebug() << "itemToResize:" << itemToResize.itemPath;

    m_itemToResize = itemToResize;
    m_itemToResizePath = path;

    if ( preferredSize > maxSize )
    {
        preferredSize = maxSize;
    }

    qint64 newSize = m_itemToResize.size - preferredSize;
    m_itemToResize.size = preferredSize;
    int opCount = countTransform( m_items,
                                  [ preferredSize ]( PartitionSplitterItem& item ) -> bool
                                  {
                                      if ( item.status == PartitionSplitterItem::Resizing )
                                      {
                                          item.size = preferredSize;
                                          return true;
                                      }
                                      return false;
                                  } );
    cDebug() << "each splitter item opcount:" << opCount;
    m_itemMinSize = minSize;
    m_itemMaxSize = maxSize;
    m_itemPrefSize = preferredSize;

    for ( int i = 0; i < m_items.count(); ++i )
    {
        if ( m_items[ i ].itemPath == itemToResize.itemPath )
        {
            m_items.insert( i + 1,
                            { "", QColor( "#c0392b" ), false, newSize, PartitionSplitterItem::ResizingNext, {} } );
            m_itemToResizeNext = m_items[ i + 1 ];
            break;
        }
        else if ( !m_items[ i ].children.isEmpty() )
        {
            for ( int j = 0; j < m_items[ i ].children.count(); ++j )
            {
                if ( m_items[ i ].children[ j ].itemPath == itemToResize.itemPath )
                {
                    m_items[ i ].children.insert(
                        j + 1, { "", QColor( "#c0392b" ), false, newSize, PartitionSplitterItem::ResizingNext, {} } );
                    m_itemToResizeNext = m_items[ i ].children[ j + 1 ];
                    break;
                }
            }
            if ( !m_itemToResizeNext.isNull() )
            {
                break;
            }
        }
    }

    Q_EMIT partitionResized( m_itemToResize.itemPath, m_itemToResize.size, m_itemToResizeNext.size );

    cDebug() << "Items updated. Status:";
    foreach ( const PartitionSplitterItem& item, m_items )
    {
        cDebug() << "item" << item.itemPath << "size" << item.size << "status:" << item.status;
    }

    cDebug() << "m_itemToResize:    " << !m_itemToResize.isNull() << m_itemToResize.itemPath;
    cDebug() << "m_itemToResizeNext:" << !m_itemToResizeNext.isNull() << m_itemToResizeNext.itemPath;

    repaint();
}

qint64
PartitionSplitterWidget::splitPartitionSize() const
{
    if ( !m_itemToResize )
    {
        return -1;
    }
    return m_itemToResize.size;
}

qint64
PartitionSplitterWidget::newPartitionSize() const
{
    if ( !m_itemToResizeNext )
    {
        return -1;
    }
    return m_itemToResizeNext.size;
}

QSize
PartitionSplitterWidget::sizeHint() const
{
    return QSize( -1, VIEW_HEIGHT );
}

QSize
PartitionSplitterWidget::minimumSizeHint() const
{
    return sizeHint();
}

void
PartitionSplitterWidget::paintEvent( QPaintEvent* event )
{
    Q_UNUSED( event )

    QPainter painter( this );
    painter.fillRect( rect(), palette().window() );
    painter.setRenderHint( QPainter::Antialiasing );

    drawPartitions( &painter, rect(), m_items );
}

void
PartitionSplitterWidget::mousePressEvent( QMouseEvent* event )
{
    if ( m_itemToResize && m_itemToResizeNext && event->button() == Qt::LeftButton )
    {
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
        if ( qAbs( event->x() - m_resizeHandleX ) < HANDLE_SNAP )
#else
        if ( qAbs( event->position().x() - m_resizeHandleX ) < HANDLE_SNAP )
#endif
        {
            m_resizing = true;
        }
    }
}

void
PartitionSplitterWidget::mouseMoveEvent( QMouseEvent* event )
{
    if ( m_resizing )
    {
        qint64 start = 0;
        QString itemPath = m_itemToResize.itemPath;
        for ( auto it = m_items.constBegin(); it != m_items.constEnd(); ++it )
        {
            if ( it->itemPath == itemPath )
            {
                break;
            }
            else if ( !it->children.isEmpty() )
            {
                bool done = false;
                for ( auto jt = it->children.constBegin(); jt != it->children.constEnd(); ++jt )
                {
                    if ( jt->itemPath == itemPath )
                    {
                        done = true;
                        break;
                    }
                    start += jt->size;
                }
                if ( done )
                {
                    break;
                }
            }
            else
            {
                start += it->size;
            }
        }

        qint64 total = 0;
        for ( auto it = m_items.constBegin(); it != m_items.constEnd(); ++it )
        {
            total += it->size;
        }

        int ew = rect().width();  //effective width
        qreal bpp = total / static_cast< qreal >( ew );  //bytes per pixel

#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
        qreal mx = event->x() * bpp - start;
#else
        qreal mx = event->position().x() * bpp - start;
#endif

        // make sure we are within resize range
        mx = qBound( static_cast< qreal >( m_itemMinSize ), mx, static_cast< qreal >( m_itemMaxSize ) );

        qint64 span = m_itemPrefSize;
        qreal percent = mx / span;
        qint64 oldsize = m_itemToResize.size;

        m_itemToResize.size = qRound64( span * percent );
        m_itemToResizeNext.size -= m_itemToResize.size - oldsize;
        countTransform( m_items,
                        [ this ]( PartitionSplitterItem& item ) -> bool
                        {
                            if ( item.status == PartitionSplitterItem::Resizing )
                            {
                                item.size = m_itemToResize.size;
                                return true;
                            }
                            else if ( item.status == PartitionSplitterItem::ResizingNext )
                            {
                                item.size = m_itemToResizeNext.size;
                                return true;
                            }
                            return false;
                        } );

        repaint();

        Q_EMIT partitionResized( itemPath, m_itemToResize.size, m_itemToResizeNext.size );
    }
    else
    {
        if ( m_itemToResize && m_itemToResizeNext )
        {
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
            if ( qAbs( event->x() - m_resizeHandleX ) < HANDLE_SNAP )
#else
            if ( qAbs( event->position().x() - m_resizeHandleX ) < HANDLE_SNAP )
#endif
            {
                setCursor( Qt::SplitHCursor );
            }
            else if ( cursor().shape() != Qt::ArrowCursor )
            {
                setCursor( Qt::ArrowCursor );
            }
        }
    }
}

void
PartitionSplitterWidget::mouseReleaseEvent( QMouseEvent* event )
{
    Q_UNUSED( event )

    m_resizing = false;
}

void
PartitionSplitterWidget::drawSection( QPainter* painter,
                                      const QRect& rect_,
                                      int x,
                                      int width,
                                      const PartitionSplitterItem& item )
{
    QColor color = item.color;
    bool isFreeSpace = item.isFreeSpace;

    QRect rect = rect_;
    const int y = rect.y();
    const int rectHeight = rect.height();
    const int radius = qMax( 1, CORNER_RADIUS - ( height() - rectHeight ) / 2 );
    painter->setClipRect( x, y, width, rectHeight );
    painter->translate( 0.5, 0.5 );

    rect.adjust( 0, 0, -1, -1 );
    const QColor borderColor = color.darker();
    painter->setPen( borderColor );
    painter->setBrush( color );
    painter->drawRoundedRect( rect, radius, radius );

    // Draw shade
    if ( !isFreeSpace )
    {
        rect.adjust( 2, 2, -2, -2 );
    }

    QLinearGradient gradient( 0, 0, 0, rectHeight / 2 );

    qreal c = isFreeSpace ? 0 : 1;
    gradient.setColorAt( 0, QColor::fromRgbF( c, c, c, 0.3 ) );
    gradient.setColorAt( 1, QColor::fromRgbF( c, c, c, 0 ) );

    painter->setPen( Qt::NoPen );
    painter->setBrush( gradient );
    painter->drawRoundedRect( rect, radius, radius );

    painter->translate( -0.5, -0.5 );
}

void
PartitionSplitterWidget::drawResizeHandle( QPainter* painter, const QRect& rect_, int x )
{
    if ( !m_itemToResize )
    {
        return;
    }

    painter->setPen( Qt::NoPen );
    painter->setBrush( Qt::black );
    painter->setClipRect( rect_ );

    painter->setRenderHint( QPainter::Antialiasing, true );

    qreal h = VIEW_HEIGHT;  // Put the arrow in the center regardless of inner box height
    int scaleFactor = qRound( height() / static_cast< qreal >( VIEW_HEIGHT ) );
    QList< QPair< qreal, qreal > > arrow_offsets
        = { qMakePair( 0, h / 2 - 1 ), qMakePair( 4, h / 2 - 1 ), qMakePair( 4, h / 2 - 3 ), qMakePair( 8, h / 2 ),
            qMakePair( 4, h / 2 + 3 ), qMakePair( 4, h / 2 + 1 ), qMakePair( 0, h / 2 + 1 ) };
    for ( int i = 0; i < arrow_offsets.count(); ++i )
    {
        arrow_offsets[ i ] = qMakePair( arrow_offsets[ i ].first * scaleFactor,
                                        ( arrow_offsets[ i ].second - h / 2 ) * scaleFactor + h / 2 );
    }

    auto p1 = arrow_offsets[ 0 ];
    if ( m_itemToResize.size > m_itemMinSize )
    {
        auto arrow = QPainterPath( QPointF( x + -1 * p1.first, p1.second ) );
        for ( auto p : arrow_offsets )
        {
            arrow.lineTo( x + -1 * p.first + 1, p.second );
        }
        painter->drawPath( arrow );
    }

    if ( m_itemToResize.size < m_itemMaxSize )
    {
        auto arrow = QPainterPath( QPointF( x + p1.first, p1.second ) );
        for ( auto p : arrow_offsets )
        {
            arrow.lineTo( x + p.first, p.second );
        }
        painter->drawPath( arrow );
    }

    painter->setRenderHint( QPainter::Antialiasing, false );
    painter->setPen( Qt::black );
    painter->drawLine( x, 0, x, int( h ) - 1 );
}

void
PartitionSplitterWidget::drawPartitions( QPainter* painter,
                                         const QRect& rect,
                                         const QVector< PartitionSplitterItem >& itemList )
{
    const int count = itemList.count();
    const int totalWidth = rect.width();

    auto pair = computeItemsVector( itemList );
    QVector< PartitionSplitterItem >& items = pair.first;
    qreal total = pair.second;

    int x = rect.x();
    for ( int row = 0; row < count; ++row )
    {
        const PartitionSplitterItem& item = items[ row ];
        qreal width;
        if ( row < count - 1 )
        {
            width = totalWidth * ( item.size / total );
        }
        else
        // Make sure we fill the last pixel column
        {
            width = rect.right() - x + 1;
        }

        drawSection( painter, rect, x, int( width ), item );
        if ( !item.children.isEmpty() )
        {
            QRect subRect( x + EXTENDED_PARTITION_MARGIN,
                           rect.y() + EXTENDED_PARTITION_MARGIN,
                           int( width ) - 2 * EXTENDED_PARTITION_MARGIN,
                           rect.height() - 2 * EXTENDED_PARTITION_MARGIN );
            drawPartitions( painter, subRect, item.children );
        }

        // If an item to resize and the following new item both exist,
        // and this is not the very first partition,
        // and the partition preceding this one is the item to resize...
        if ( m_itemToResize && m_itemToResizeNext && row > 0 && !items[ row - 1 ].isFreeSpace
             && !items[ row - 1 ].itemPath.isEmpty() && items[ row - 1 ].itemPath == m_itemToResize.itemPath )
        {
            m_resizeHandleX = x;
            drawResizeHandle( painter, rect, m_resizeHandleX );
        }

        x += width;
    }
}

QPair< QVector< PartitionSplitterItem >, qreal >
PartitionSplitterWidget::computeItemsVector( const QVector< PartitionSplitterItem >& originalItems ) const
{
    QVector< PartitionSplitterItem > items;

    qreal total = 0;
    for ( int row = 0; row < originalItems.count(); ++row )
    {
        if ( originalItems[ row ].children.isEmpty() )
        {
            items += originalItems[ row ];
            total += originalItems[ row ].size;
        }
        else
        {
            PartitionSplitterItem thisItem = originalItems[ row ];
            QPair< QVector< PartitionSplitterItem >, qreal > pair = computeItemsVector( thisItem.children );
            thisItem.children = pair.first;
            thisItem.size = qint64( pair.second );
            items += thisItem;
            total += thisItem.size;
        }
    }

    // The sizes we have are perfect, but now we have to hardcode a minimum size for small
    // partitions and compensate for it in the total.
    qreal adjustedTotal = total;
    for ( int row = 0; row < items.count(); ++row )
    {
        if ( items[ row ].size < 0.01 * total )  // If this item is smaller than 1% of everything,
        {
            // force its width to 1%.
            adjustedTotal -= items[ row ].size;
            items[ row ].size = qint64( 0.01 * total );
            adjustedTotal += items[ row ].size;
        }
    }

    return qMakePair( items, adjustedTotal );
}
