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

#include "highlightingitemdelegate.h"

#include <QApplication>
#include <QModelIndex>
#include <QPainter>

const int kMinimumLineNumberDigits = 6;

namespace Utils {

HighlightingItemDelegate::HighlightingItemDelegate(int tabWidth, QObject *parent)
    : QItemDelegate(parent)
{
    setTabWidth(tabWidth);
}

void HighlightingItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
                                     const QModelIndex &index) const
{
    static const int iconSize = 16;

    painter->save();

    const QStyleOptionViewItem opt = setOptions(index, option);
    painter->setFont(opt.font);

    QItemDelegate::drawBackground(painter, opt, index);

    // ---- do the layout
    QRect checkRect;
    QRect pixmapRect;
    QRect textRect;

    // check mark
    const bool checkable = (index.model()->flags(index) & Qt::ItemIsUserCheckable);
    Qt::CheckState checkState = Qt::Unchecked;
    if (checkable) {
        QVariant checkStateData = index.data(Qt::CheckStateRole);
        checkState = static_cast<Qt::CheckState>(checkStateData.toInt());
        checkRect = doCheck(opt, opt.rect, checkStateData);
    }

    // icon
    const QIcon icon = index.model()->data(index, Qt::DecorationRole).value<QIcon>();
    if (!icon.isNull()) {
        const QSize size = icon.actualSize(QSize(iconSize, iconSize));
        pixmapRect = QRect(0, 0, size.width(), size.height());
    }

    // text
    textRect = opt.rect.adjusted(0, 0, checkRect.width() + pixmapRect.width(), 0);

    // do layout
    doLayout(opt, &checkRect, &pixmapRect, &textRect, false);
    // ---- draw the items
    // icon
    if (!icon.isNull())
        icon.paint(painter, pixmapRect, option.decorationAlignment);

    // line numbers
    const int lineNumberAreaWidth = drawLineNumber(painter, opt, textRect, index);
    textRect.adjust(lineNumberAreaWidth, 0, 0, 0);

    // text and focus/selection
    drawText(painter, opt, textRect, index);
    QItemDelegate::drawFocus(painter, opt, opt.rect);

    // check mark
    if (checkable)
        QItemDelegate::drawCheck(painter, opt, checkRect, checkState);

    painter->restore();
}

void HighlightingItemDelegate::setTabWidth(int width)
{
    m_tabString = QString(width, ' ');
}

// returns the width of the line number area
int HighlightingItemDelegate::drawLineNumber(QPainter *painter, const QStyleOptionViewItem &option,
                                             const QRect &rect,
                                             const QModelIndex &index) const
{
    static const int lineNumberAreaHorizontalPadding = 4;
    const int lineNumber = index.model()->data(index, int(HighlightingItemRole::LineNumber)).toInt();
    if (lineNumber < 1)
        return 0;
    const bool isSelected = option.state & QStyle::State_Selected;
    const QString lineText = QString::number(lineNumber);
    const int minimumLineNumberDigits = qMax(kMinimumLineNumberDigits, lineText.size());
    const int fontWidth =
        painter->fontMetrics().horizontalAdvance(QString(minimumLineNumberDigits, '0'));
    const int lineNumberAreaWidth = lineNumberAreaHorizontalPadding + fontWidth
                                    + lineNumberAreaHorizontalPadding;
    QRect lineNumberAreaRect(rect);
    lineNumberAreaRect.setWidth(lineNumberAreaWidth);

    QPalette::ColorGroup cg = QPalette::Normal;
    if (!(option.state & QStyle::State_Active))
        cg = QPalette::Inactive;
    else if (!(option.state & QStyle::State_Enabled))
        cg = QPalette::Disabled;

    painter->fillRect(lineNumberAreaRect, QBrush(isSelected ?
        option.palette.brush(cg, QPalette::Highlight) :
        option.palette.color(cg, QPalette::Base).darker(111)));

    QStyleOptionViewItem opt = option;
    opt.displayAlignment = Qt::AlignRight | Qt::AlignVCenter;
    opt.palette.setColor(cg, QPalette::Text, Qt::darkGray);

    const QStyle *style = QApplication::style();
    const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, nullptr) + 1;

    const QRect rowRect
            = lineNumberAreaRect.adjusted(-textMargin, 0,
                                          textMargin - lineNumberAreaHorizontalPadding, 0);
    QItemDelegate::drawDisplay(painter, opt, rowRect, lineText);

    return lineNumberAreaWidth;
}

void HighlightingItemDelegate::drawText(QPainter *painter,
                                        const QStyleOptionViewItem &option,
                                        const QRect &rect,
                                        const QModelIndex &index) const
{
    QString text = index.model()->data(index, Qt::DisplayRole).toString();
    // show number of subresults in displayString
    if (index.model()->hasChildren(index))
        text += " (" + QString::number(index.model()->rowCount(index)) + ')';

    QList<int> searchTermStarts
        = index.model()->data(index, int(HighlightingItemRole::StartColumn)).value<QList<int>>();
    QList<int> searchTermLengths
        = index.model()->data(index, int(HighlightingItemRole::Length)).value<QList<int>>();

    QList<QTextLayout::FormatRange> formats;

    const QString extraText
        = index.model()->data(index, int(HighlightingItemRole::DisplayExtra)).toString();
    if (!extraText.isEmpty()) {
        if (!option.state.testFlag(QStyle::State_Selected)) {
            int start = text.length();
            auto dataType = int(HighlightingItemRole::DisplayExtraForeground);
            const QColor highlightForeground = index.model()->data(index, dataType).value<QColor>();
            QTextCharFormat extraFormat;
            extraFormat.setForeground(highlightForeground);
            formats.append({start, int(extraText.length()), extraFormat});
        }
        text.append(extraText);
    }

    if (searchTermStarts.isEmpty()) {
        drawDisplay(painter, option, rect, text.replace('\t', m_tabString), formats);
        return;
    }

    // replace tabs with searchTerm bookkeeping
    const int tabDiff = m_tabString.size() - 1;
    for (int i = 0; i < text.length(); i++) {
        if (text.at(i) != '\t')
            continue;

        text.replace(i, 1, m_tabString);

        // adjust highlighting length if tab is highlighted
        for (int j = 0; j < searchTermStarts.size(); ++j) {
            if (i >= searchTermStarts.at(j) && i < searchTermStarts.at(j) + searchTermLengths.at(j))
                searchTermLengths[j] += tabDiff;
        }

        // adjust all following highlighting starts
        for (int j = 0; j < searchTermStarts.size(); ++j) {
            if (searchTermStarts.at(j) > i)
                searchTermStarts[j] += tabDiff;
        }

        i += tabDiff;
    }

    const QColor highlightForeground =
            index.model()->data(index, int(HighlightingItemRole::Foreground)).value<QColor>();
    const QColor highlightBackground =
            index.model()->data(index, int(HighlightingItemRole::Background)).value<QColor>();
    QTextCharFormat highlightFormat;
    highlightFormat.setForeground(highlightForeground);
    highlightFormat.setBackground(highlightBackground);

    for (int i = 0, size = searchTermStarts.size(); i < size; ++i)
        formats.append({searchTermStarts.at(i), searchTermLengths.at(i), highlightFormat});

    drawDisplay(painter, option, rect, text, formats);
}

// copied from QItemDelegate for drawDisplay
static QString replaceNewLine(QString text)
{
    static const QChar nl = '\n';
    for (int i = 0; i < text.size(); ++i)
        if (text.at(i) == nl)
            text[i] = QChar::LineSeparator;
    return text;
}

// copied from QItemDelegate for drawDisplay
QSizeF doTextLayout(QTextLayout *textLayout, int lineWidth)
{
    qreal height = 0;
    qreal widthUsed = 0;
    textLayout->beginLayout();
    while (true) {
        QTextLine line = textLayout->createLine();
        if (!line.isValid())
            break;
        line.setLineWidth(lineWidth);
        line.setPosition(QPointF(0, height));
        height += line.height();
        widthUsed = qMax(widthUsed, line.naturalTextWidth());
    }
    textLayout->endLayout();
    return QSizeF(widthUsed, height);
}

// copied from QItemDelegate to be able to add the 'format' parameter
void HighlightingItemDelegate::drawDisplay(QPainter *painter,
                                           const QStyleOptionViewItem &option,
                                           const QRect &rect, const QString &text,
                                           const QList<QTextLayout::FormatRange> &format) const
{
    QPalette::ColorGroup cg = option.state & QStyle::State_Enabled
                              ? QPalette::Normal : QPalette::Disabled;
    if (cg == QPalette::Normal && !(option.state & QStyle::State_Active))
        cg = QPalette::Inactive;
    if (option.state & QStyle::State_Selected) {
        painter->fillRect(rect, option.palette.brush(cg, QPalette::Highlight));
        painter->setPen(option.palette.color(cg, QPalette::HighlightedText));
    } else {
        painter->setPen(option.palette.color(cg, QPalette::Text));
    }

    if (text.isEmpty())
        return;

    if (option.state & QStyle::State_Editing) {
        painter->save();
        painter->setPen(option.palette.color(cg, QPalette::Text));
        painter->drawRect(rect.adjusted(0, 0, -1, -1));
        painter->restore();
    }

    const QStyleOptionViewItem opt = option;

    const QWidget *widget = option.widget;
    QStyle *style = widget ? widget->style() : QApplication::style();
    const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, widget) + 1;
    QRect textRect = rect.adjusted(textMargin, 0, -textMargin, 0); // remove width padding
    const bool wrapText = opt.features & QStyleOptionViewItem::WrapText;
    QTextOption textOption;
    textOption.setWrapMode(wrapText ? QTextOption::WordWrap : QTextOption::ManualWrap);
    textOption.setTextDirection(option.direction);
    textOption.setAlignment(QStyle::visualAlignment(option.direction, option.displayAlignment));
    QTextLayout textLayout;
    textLayout.setTextOption(textOption);
    textLayout.setFont(option.font);
    textLayout.setText(replaceNewLine(text));

    QSizeF textLayoutSize = doTextLayout(&textLayout, textRect.width());

    if (textRect.width() < textLayoutSize.width()
        || textRect.height() < textLayoutSize.height()) {
        QString elided;
        int start = 0;
        int end = text.indexOf(QChar::LineSeparator, start);
        if (end == -1) {
            elided += option.fontMetrics.elidedText(text, option.textElideMode, textRect.width());
        } else {
            while (end != -1) {
                elided += option.fontMetrics.elidedText(text.mid(start, end - start),
                                                        option.textElideMode, textRect.width());
                elided += QChar::LineSeparator;
                start = end + 1;
                end = text.indexOf(QChar::LineSeparator, start);
            }
            // let's add the last line (after the last QChar::LineSeparator)
            elided += option.fontMetrics.elidedText(text.mid(start),
                                                    option.textElideMode, textRect.width());
        }
        textLayout.setText(elided);
        textLayoutSize = doTextLayout(&textLayout, textRect.width());
    }

    const QSize layoutSize(textRect.width(), int(textLayoutSize.height()));
    const QRect layoutRect = QStyle::alignedRect(option.direction, option.displayAlignment,
                                                  layoutSize, textRect);
    // if we still overflow even after eliding the text, enable clipping
    if (!hasClipping() && (textRect.width() < textLayoutSize.width()
                           || textRect.height() < textLayoutSize.height())) {
        painter->save();
        painter->setClipRect(layoutRect);
        textLayout.draw(painter, layoutRect.topLeft(), format, layoutRect);
        painter->restore();
    } else {
        textLayout.draw(painter, layoutRect.topLeft(), format, layoutRect);
    }
}

} // namespace Utils
