// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <private/legendlayout_p.h>
#include <private/chartpresenter_p.h>
#include <private/qlegend_p.h>
#include <private/abstractchartlayout_p.h>

#include <private/qlegendmarker_p.h>
#include <private/legendmarkeritem_p.h>
#include <QtCharts/QLegendMarker>

QT_BEGIN_NAMESPACE

LegendLayout::LegendLayout(QLegend *legend)
    : m_legend(legend),
      m_offsetX(0),
      m_offsetY(0)
{

}

LegendLayout::~LegendLayout()
{

}

void LegendLayout::setOffset(qreal x, qreal y)
{
    bool scrollHorizontal = true;
    switch (m_legend->alignment()) {
    case Qt::AlignTop:
    case Qt::AlignBottom:
        scrollHorizontal = true;
        break;
    case Qt::AlignLeft:
    case Qt::AlignRight:
        scrollHorizontal = false;
        break;
    }

    // If detached, the scrolling direction is vertical instead of horizontal and vice versa.
    if (!m_legend->isAttachedToChart())
        scrollHorizontal = !scrollHorizontal;

    QRectF boundingRect = geometry();
    qreal left, top, right, bottom;
    getContentsMargins(&left, &top, &right, &bottom);
    boundingRect.adjust(left, top, -right, -bottom);

    // Limit offset between m_minOffset and m_maxOffset
    if (scrollHorizontal) {
        if (m_width <= boundingRect.width())
            return;

        if (x != m_offsetX) {
            m_offsetX = qBound(m_minOffsetX, x, m_maxOffsetX);
            m_legend->d_ptr->items()->setPos(-m_offsetX, boundingRect.top());
        }
    } else {
        if (m_height <= boundingRect.height())
            return;

        if (y != m_offsetY) {
            m_offsetY = qBound(m_minOffsetY, y, m_maxOffsetY);
            m_legend->d_ptr->items()->setPos(boundingRect.left(), -m_offsetY);
        }
    }
}

QPointF LegendLayout::offset() const
{
    return QPointF(m_offsetX, m_offsetY);
}

void LegendLayout::invalidate()
{
    QGraphicsLayout::invalidate();
    if (m_legend->isAttachedToChart())
        m_legend->d_ptr->m_presenter->layout()->invalidate();
}

void LegendLayout::setGeometry(const QRectF &rect)
{
    m_legend->d_ptr->items()->setVisible(m_legend->isVisible());

    QGraphicsLayout::setGeometry(rect);

    if (m_legend->isAttachedToChart())
        setAttachedGeometry(rect);
    else
        setDettachedGeometry(rect);
}

void LegendLayout::setAttachedGeometry(const QRectF &rect)
{
    if (!rect.isValid())
        return;

    qreal oldOffsetX = m_offsetX;
    qreal oldOffsetY = m_offsetY;
    m_offsetX = 0;
    m_offsetY = 0;

    QSizeF size(0, 0);

    if (m_legend->d_ptr->markers().isEmpty()) {
        return;
    }

    m_width = 0;
    m_height = 0;

    qreal left, top, right, bottom;
    getContentsMargins(&left, &top, &right, &bottom);

    QRectF geometry = rect.adjusted(left, top, -right, -bottom);

    switch(m_legend->alignment()) {
    case Qt::AlignTop:
    case Qt::AlignBottom: {
            // Calculate the space required for items and add them to a sorted list.
            qreal markerItemsWidth = 0;
            qreal itemMargins = 0;
            QList<LegendWidthStruct *> legendWidthList;
            const auto markers = m_legend->d_ptr->markers();
            for (QLegendMarker *marker : markers) {
                LegendMarkerItem *item = marker->d_ptr->item();
                if (item->isVisible()) {
                    QSizeF dummySize;
                    qreal itemWidth = item->sizeHint(Qt::PreferredSize, dummySize).width();
                    LegendWidthStruct *structItem = new LegendWidthStruct;
                    structItem->item = item;
                    structItem->width = itemWidth;
                    legendWidthList.append(structItem);
                    markerItemsWidth += itemWidth;
                    itemMargins += marker->d_ptr->item()->m_margin;
                }
            }
            std::sort(legendWidthList.begin(), legendWidthList.end(), widthLongerThan);

            // If the items would occupy more space than is available, start truncating them
            // from the longest one.
            qreal availableGeometry = geometry.width() - itemMargins;
            if (markerItemsWidth >= availableGeometry && legendWidthList.size() > 0) {
                bool truncated(false);
                int count = legendWidthList.size();
                for (int i = 1; i < count; i++) {
                    int truncateIndex = i - 1;

                    while (legendWidthList.at(truncateIndex)->width >= legendWidthList.at(i)->width
                           && !truncated) {
                        legendWidthList.at(truncateIndex)->width--;
                        markerItemsWidth--;
                        if (i > 1) {
                            // Truncate the items that are before the truncated one in the list.
                            for (int j = truncateIndex - 1; j >= 0; j--) {
                                if (legendWidthList.at(truncateIndex)->width
                                        < legendWidthList.at(j)->width) {
                                    legendWidthList.at(j)->width--;
                                    markerItemsWidth--;
                                }
                            }
                        }
                        if (markerItemsWidth < availableGeometry)
                            truncated = true;
                    }
                    // Truncate the last item if needed.
                    if (i == count - 1) {
                        if (legendWidthList.at(count - 1)->width
                                > legendWidthList.at(truncateIndex)->width) {
                            legendWidthList.at(count - 1)->width--;
                            markerItemsWidth--;
                        }
                    }

                    if (truncated)
                        break;
                }
                // Items are of same width and all of them need to be truncated
                // or there is just one item that is truncated.
                while (markerItemsWidth >= availableGeometry) {
                    for (int i = 0; i < count; i++) {
                        legendWidthList.at(i)->width--;
                        markerItemsWidth--;
                    }
                }
            }

            QPointF point(0,0);

            int markerCount = m_legend->d_ptr->markers().size();
            for (int i = 0; i < markerCount; i++) {
                QLegendMarker *marker;
                if (m_legend->d_ptr->m_reverseMarkers)
                    marker = m_legend->d_ptr->markers().at(markerCount - 1 - i);
                else
                    marker = m_legend->d_ptr->markers().at(i);
                LegendMarkerItem *item = marker->d_ptr->item();
                if (item->isVisible()) {
                    QRectF itemRect = geometry;
                    qreal availableWidth = 0;
                    for (int i = 0; i < legendWidthList.size(); ++i) {
                        if (legendWidthList.at(i)->item == item) {
                            availableWidth = legendWidthList.at(i)->width;
                            break;
                        }
                    }
                    itemRect.setWidth(availableWidth);
                    item->setGeometry(itemRect);
                    item->setPos(point.x(),geometry.height()/2 - item->boundingRect().height()/2);
                    const QRectF &rect = item->boundingRect();
                    size = size.expandedTo(rect.size());
                    qreal w = rect.width();
                    m_width = m_width + w - item->m_margin;
                    point.setX(point.x() + w);
                }
            }
            // Delete structs from the container
            qDeleteAll(legendWidthList);

            // Round to full pixel via QPoint to avoid one pixel clipping on the edge in some cases
            if (m_width < geometry.width()) {
                m_legend->d_ptr->items()->setPos(QPoint(geometry.width() / 2 - m_width / 2,
                                                        geometry.top()));
            } else {
                m_legend->d_ptr->items()->setPos(geometry.topLeft().toPoint());
            }
            m_height = size.height();
        }
        break;
    case Qt::AlignLeft:
    case Qt::AlignRight: {
            QPointF point(0,0);
            int markerCount = m_legend->d_ptr->markers().size();
            for (int i = 0; i < markerCount; i++) {
                QLegendMarker *marker;
                if (m_legend->d_ptr->m_reverseMarkers)
                    marker = m_legend->d_ptr->markers().at(markerCount - 1 - i);
                else
                    marker = m_legend->d_ptr->markers().at(i);
                LegendMarkerItem *item = marker->d_ptr->item();
                if (item->isVisible()) {
                    item->setGeometry(geometry);
                    item->setPos(point);
                    const QRectF &rect = item->boundingRect();
                    qreal h = rect.height();
                    size = size.expandedTo(rect.size());
                    m_height+=h;
                    point.setY(point.y() + h);
                }
            }

            // Round to full pixel via QPoint to avoid one pixel clipping on the edge in some cases
            if (m_height < geometry.height()) {
                m_legend->d_ptr->items()->setPos(QPoint(geometry.left(),
                                                        geometry.height() / 2 - m_height / 2));
            } else {
                m_legend->d_ptr->items()->setPos(geometry.topLeft().toPoint());
            }
            m_width = size.width();
            break;
            }
        }

    m_minOffsetX = -left;
    m_minOffsetY = - top;
    m_maxOffsetX = m_width - geometry.width() - right;
    m_maxOffsetY = m_height - geometry.height() - bottom;

    setOffset(oldOffsetX, oldOffsetY);
}

void LegendLayout::setDettachedGeometry(const QRectF &rect)
{
    if (!rect.isValid())
        return;

    // Detached layout is different.
    // In detached mode legend may have multiple rows and columns, so layout calculations
    // differ a log from attached mode.
    // Also the scrolling logic is bit different.

    qreal oldOffsetX = m_offsetX;
    qreal oldOffsetY = m_offsetY;
    m_offsetX = 0;
    m_offsetY = 0;

    qreal left, top, right, bottom;
    getContentsMargins(&left, &top, &right, &bottom);
    QRectF geometry = rect.adjusted(left, top, -right, -bottom);

    QList<QLegendMarker *> markers = m_legend->d_ptr->markers();

    if (markers.isEmpty())
        return;

    Qt::Alignment align = m_legend->alignment();
    switch (align) {
    case Qt::AlignTop:
    case Qt::AlignBottom: {
        QPointF point(0, (align == Qt::AlignTop) ? 0 : geometry.height());
        m_width = 0;
        m_height = 0;
        for (int i = 0; i < markers.size(); i++) {
            LegendMarkerItem *item = markers.at(i)->d_ptr->item();
            if (item->isVisible()) {
                item->setGeometry(geometry);
                const QRectF &boundingRect = item->boundingRect();
                qreal w = boundingRect.width();
                qreal h = boundingRect.height();
                m_width = qMax(m_width,w);
                m_height = qMax(m_height,h);
                item->setPos(point.x(),point.y() - ((align == Qt::AlignTop) ? 0 : h));
                point.setX(point.x() + w);
                if (point.x() + w > geometry.left() + geometry.width() - right) {
                    // Next item would go off rect.
                    point.setX(0);
                    point.setY(point.y() + ((align == Qt::AlignTop) ? h : -h));
                    if (i+1 < markers.size()) {
                        m_height += h;
                    }
                }
            }
        }
        m_legend->d_ptr->items()->setPos(geometry.topLeft());

        m_minOffsetX = -left;
        m_maxOffsetX = m_width - geometry.width() - right;
        if (align == Qt::AlignTop) {
            m_minOffsetY = -top;
            m_maxOffsetY = m_height - geometry.height() - bottom;
        } else {
            m_minOffsetY = -m_height + geometry.height() - top;
            m_maxOffsetY = -bottom;
        }
    }
    break;
    case Qt::AlignLeft:
    case Qt::AlignRight: {
        QPointF point((align == Qt::AlignLeft) ? 0 : geometry.width(), 0);
        m_width = 0;
        m_height = 0;
        qreal maxWidth = 0;
        for (int i = 0; i < markers.size(); i++) {
            LegendMarkerItem *item = markers.at(i)->d_ptr->item();
            if (item->isVisible()) {
                item->setGeometry(geometry);
                const QRectF &boundingRect = item->boundingRect();
                qreal w = boundingRect.width();
                qreal h = boundingRect.height();
                m_height = qMax(m_height,h);
                maxWidth = qMax(maxWidth,w);
                item->setPos(point.x() - ((align == Qt::AlignLeft) ? 0 : w),point.y());
                point.setY(point.y() + h);
                if (point.y() + h > geometry.bottom()-bottom) {
                    // Next item would go off rect.
                    point.setX(point.x() + ((align == Qt::AlignLeft) ? maxWidth : -maxWidth));
                    point.setY(0);
                    if (i+1 < markers.size()) {
                        m_width += maxWidth;
                        maxWidth = 0;
                    }
                }
            }
        }
        m_width += maxWidth;
        m_legend->d_ptr->items()->setPos(geometry.topLeft());

        m_minOffsetY = -top;
        m_maxOffsetY = m_height - geometry.height() - bottom;
        if (align == Qt::AlignLeft) {
            m_minOffsetX = -left;
            m_maxOffsetX = m_width - geometry.width() - right;
        } else {
            m_minOffsetX = - m_width + geometry.width() - left;
            m_maxOffsetX = - right;
        }
    }
    break;
    default:
        break;
    }

    setOffset(oldOffsetX, oldOffsetY);
}

QSizeF LegendLayout::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const
{
    QSizeF size(0, 0);
    qreal left, top, right, bottom;
    getContentsMargins(&left, &top, &right, &bottom);

    if(constraint.isValid()) {
        const auto markers = m_legend->d_ptr->markers();
        for (QLegendMarker *marker : markers) {
            LegendMarkerItem *item = marker->d_ptr->item();
            size = size.expandedTo(item->effectiveSizeHint(which));
        }
        size = size.boundedTo(constraint);
    }
    else if (constraint.width() >= 0) {
        qreal width = 0;
        qreal height = 0;
        const auto markers = m_legend->d_ptr->markers();
        for (QLegendMarker *marker : markers) {
            LegendMarkerItem *item = marker->d_ptr->item();
            width+=item->effectiveSizeHint(which).width();
            height=qMax(height,item->effectiveSizeHint(which).height());
        }

        size = QSizeF(qMin(constraint.width(),width), height);
    }
    else if (constraint.height() >= 0) {
        qreal width = 0;
        qreal height = 0;
        const auto markers = m_legend->d_ptr->markers();
        for (QLegendMarker *marker : markers) {
            LegendMarkerItem *item = marker->d_ptr->item();
            width=qMax(width,item->effectiveSizeHint(which).width());
            height+=height,item->effectiveSizeHint(which).height();
        }
        size = QSizeF(width,qMin(constraint.height(),height));
    }
    else {
        const auto markers = m_legend->d_ptr->markers();
        for (QLegendMarker *marker : markers) {
            LegendMarkerItem *item = marker->d_ptr->item();
            size = size.expandedTo(item->effectiveSizeHint(which));
        }
    }
    size += QSize(left + right, top + bottom);
    return size;
}

bool LegendLayout::widthLongerThan(const LegendWidthStruct *item1,
                                   const LegendWidthStruct *item2)
{
    return item1->width > item2->width;
}

QT_END_NAMESPACE
