/*
 * Fooyin
 * Copyright © 2022, Luke Taylor <luket@pm.me>
 *
 * Fooyin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fooyin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "playlistdelegate.h"

#include "playlistitem.h"

#include <gui/iconloader.h>
#include <gui/scripting/richtextutils.h>
#include <gui/widgets/expandedtreeview.h>
#include <gui/widgets/metadatacompleter.h>
#include <utils/utils.h>

#include <QApplication>
#include <QHeaderView>
#include <QLineEdit>
#include <QPainter>

constexpr auto EditorInitialisedProperty = "fy_editor_initialised";

namespace Fooyin {
QWidget* PlaylistDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option,
                                        const QModelIndex& index) const
{
    auto* editor = qobject_cast<QLineEdit*>(QStyledItemDelegate::createEditor(parent, option, index));
    if(!editor) {
        editor = new QLineEdit(parent);
    }

    const QString writeField = index.data(PlaylistItem::Role::EditableWriteField).toString();
    if(writeField.isEmpty()) {
        return editor;
    }

    const auto playlistTrack = index.data(PlaylistItem::Role::ItemData).value<PlaylistTrack>();
    if(playlistTrack.track.metadataStore()) {
        Gui::setMetadataCompleter(editor, writeField, *playlistTrack.track.metadataStore());
    }

    return editor;
}

struct DrawTextResult
{
    QRect bound;
    int totalWidth{0};
};

template <typename Range>
DrawTextResult drawTextBlocks(QPainter* painter, const QStyleOptionViewItem& option, QRect rect, const Range& blocks,
                              Qt::Alignment alignment)
{
    DrawTextResult result;

    const QStyle* style         = option.widget ? option.widget->style() : QApplication::style();
    const auto selected         = option.state & QStyle::State_Selected;
    const auto colour           = selected ? QPalette::HighlightedText : QPalette::NoRole;
    const QColor defaultColour  = option.palette.color(QPalette::Text);
    const QColor selectedColour = option.palette.color(QPalette::HighlightedText);
    const QColor linkColour     = option.palette.color(QPalette::Link);

    for(const auto& block : blocks) {
        painter->setFont(resolvedRichTextFont(block.format, option.font));

        QColor blockColour = resolvedRichTextColour(block.format, defaultColour, linkColour);
        if(selected) {
            blockColour = selectedColour;
        }
        painter->setPen(blockColour);

        result.bound = painter->boundingRect(rect, alignment | Qt::TextWrapAnywhere, block.text);
        style->drawItemText(painter, rect, alignment, option.palette, true,
                            painter->fontMetrics().elidedText(block.text, Qt::ElideRight, rect.width()), colour);

        if(alignment & Qt::AlignRight) {
            rect.moveRight((rect.x() + rect.width()) - result.bound.width());
        }
        else {
            rect.setWidth(rect.width() - result.bound.width());
            rect.moveLeft(rect.x() + result.bound.width());
        }

        result.totalWidth += result.bound.width();
    }

    return result;
}

QRect subheaderContentRect(const QStyleOptionViewItem& option, const QModelIndex& index, int offset)
{
    QRect rect = option.rect.adjusted(offset, 0, -offset, 0);

    if(!index.data(PlaylistItem::Role::InsetSubheadersToImageColumns).toBool()) {
        return rect;
    }

    const auto* view = qobject_cast<const ExpandedTreeView*>(option.widget);
    if(!view || !view->header()) {
        return rect;
    }

    const auto* header = view->header();

    int leftLogical{-1};
    int leftPos{std::numeric_limits<int>::max()};
    int leftSize{0};

    int rightLogical{-1};
    int rightPos{std::numeric_limits<int>::min()};

    for(int logical{0}; logical < header->count(); ++logical) {
        if(header->isSectionHidden(logical)) {
            continue;
        }

        const int pos  = header->sectionViewportPosition(logical);
        const int size = header->sectionSize(logical);

        if(pos < leftPos) {
            leftPos     = pos;
            leftLogical = logical;
            leftSize    = size;
        }

        if(pos > rightPos) {
            rightPos     = pos;
            rightLogical = logical;
        }
    }

    if(leftLogical >= 0 && view->isSpanning(leftLogical)) {
        rect.setLeft(std::max(rect.left(), leftPos + leftSize + offset));
    }

    if(rightLogical >= 0 && view->isSpanning(rightLogical)) {
        rect.setRight(std::min(rect.right(), rightPos - offset));
    }

    if(rect.right() < rect.left()) {
        rect.setRight(rect.left());
    }

    return rect;
}

void paintHeader(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index)
{
    QStyleOptionViewItem opt{option};
    opt.text.clear();
    opt.icon = {};

    const QStyle* style = opt.widget ? opt.widget->style() : QApplication::style();

    style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);

    QPen linePen = painter->pen();
    linePen.setWidth(1);
    QColor lineColour = opt.palette.color(QPalette::Text);
    lineColour.setAlpha(40);
    linePen.setColor(lineColour);

    const auto title    = index.data(PlaylistItem::Role::Title).value<RichText>().blocks;
    const auto subtitle = index.data(PlaylistItem::Role::Subtitle).value<RichText>().blocks;
    const auto side     = index.data(PlaylistItem::Role::Right).value<RichText>().blocks;
    const auto info     = index.data(PlaylistItem::Role::Info).value<RichText>().blocks;
    const auto cover    = index.data(Qt::DecorationRole).value<QPixmap>();

    const QRect& rect           = opt.rect;
    const int halfWidth         = rect.width() / 2;
    static constexpr int offset = 5;

    static constexpr int coverMargin     = 10;
    const int coverSize                  = rect.height() - (2 * coverMargin);
    static constexpr int coverFrameWidth = 2;
    const int coverFrameOffset           = coverFrameWidth / 2;

    const QRect coverRect{rect.left() + coverMargin, rect.top() + coverMargin, coverSize, coverSize};
    QRect coverFrameRect = coverRect.adjusted(-coverFrameOffset, -coverFrameOffset, coverFrameWidth, coverFrameWidth);

    if(cover.isNull()) {
        coverFrameRect.setWidth(0);
    }

    const auto titleOffset = static_cast<int>(rect.height() * 0.08);
    const auto infoOffset  = static_cast<int>(rect.height() * 0.12);

    const QRect rightRect{rect.left() + halfWidth, rect.top(), halfWidth - offset, rect.height()};
    const auto [rightBound, totalRightWidth]
        = drawTextBlocks(painter, opt, rightRect, side | std::views::reverse, Qt::AlignVCenter | Qt::AlignRight);

    const int contentLeft = cover.isNull() ? rect.left() + offset : coverFrameRect.right() + (2 * offset);
    const int leftWidth   = cover.isNull() ? rect.right() - contentLeft + 1 - totalRightWidth
                                           : rect.width() - coverFrameRect.width() - totalRightWidth;

    QRect subtitleRect{contentLeft, rect.top(), leftWidth, rect.height()};
    if(totalRightWidth > 0) {
        subtitleRect.setWidth(subtitleRect.width() - (5 * offset));
    }
    const auto [subtitleBound, _]
        = drawTextBlocks(painter, opt, subtitleRect, subtitle, Qt::AlignVCenter | Qt::AlignLeft);

    const QRect titleRect{contentLeft, rect.top() + titleOffset, leftWidth, rect.height()};
    drawTextBlocks(painter, opt, titleRect, title, Qt::AlignTop);

    const QRect infoRect{contentLeft, rect.top() - infoOffset, leftWidth, rect.height()};
    drawTextBlocks(painter, opt, infoRect, info, Qt::AlignBottom);

    const QLineF headerLine(contentLeft, coverFrameRect.bottom() + coverFrameWidth, rect.right() - offset,
                            coverFrameRect.bottom() + coverFrameWidth);

    painter->setPen(linePen);
    if(!subtitle.empty() && !side.empty() && rect.width() > 160) {
        static constexpr int lineOffset = 10;

        const QLineF rightLine(subtitleBound.right() + lineOffset, subtitleBound.center().y() + 1,
                               rightBound.left() - lineOffset, rightBound.center().y() + 1);
        painter->drawLine(rightLine);
    }

    painter->drawLine(headerLine);

    if(!cover.isNull()) {
        QPen coverPen     = painter->pen();
        QColor coverColor = opt.palette.color(QPalette::Shadow);
        coverColor.setAlpha(65);
        coverPen.setColor(coverColor);
        coverPen.setWidth(coverFrameWidth);

        painter->setRenderHint(QPainter::Antialiasing);
        painter->drawRect(coverFrameRect);

        const double pixelRatio = opt.widget ? opt.widget->devicePixelRatioF() : 1.0;
        painter->drawPixmap(coverRect, Utils::scalePixmap(cover, coverSize, pixelRatio, true));
    }
}

void paintSimpleHeader(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index)
{
    QStyleOptionViewItem opt{option};
    opt.text.clear();
    opt.icon = {};

    const QStyle* style = opt.widget ? opt.widget->style() : QApplication::style();

    style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);

    QPen linePen = painter->pen();
    linePen.setWidth(1);
    QColor lineColour = opt.palette.color(QPalette::Text);
    lineColour.setAlpha(40);
    linePen.setColor(lineColour);

    const auto title    = index.data(PlaylistItem::Role::Title).value<RichText>().blocks;
    const auto subtitle = index.data(PlaylistItem::Role::Right).value<RichText>().blocks;

    const QRect& rect           = opt.rect;
    const int height            = rect.height();
    const int halfWidth         = rect.width() / 2;
    static constexpr int offset = 5;

    const QRect rightRect{rect.left() + halfWidth, rect.top(), halfWidth - offset, height};
    auto [rightBound, totalRightWidth]
        = drawTextBlocks(painter, opt, rightRect, subtitle | std::views::reverse, Qt::AlignVCenter | Qt::AlignRight);

    QRect leftRect{rect.left() + offset, rect.top(), rect.width() - totalRightWidth, height};
    if(totalRightWidth > 0) {
        leftRect.setWidth(leftRect.width() - (4 * offset));
    }
    auto [leftBound, _] = drawTextBlocks(painter, opt, leftRect, title, Qt::AlignVCenter | Qt::AlignLeft);

    if(!title.empty()) {
        if(subtitle.empty()) {
            rightBound = {rect.right() - offset, rect.top(), offset, height};
        }

        const int lineOffset = subtitle.empty() ? 5 : 10;

        const QLineF rightLine(leftBound.right() + lineOffset, leftBound.center().y() + 1,
                               rightBound.left() - lineOffset, rightBound.center().y() + 1);

        painter->setPen(lineColour);
        painter->drawLine(rightLine);
    }
}

void paintSubheader(QPainter* painter, const QStyleOptionViewItem& opt, const QModelIndex& index)
{
    const QStyle* style = opt.widget ? opt.widget->style() : QApplication::style();

    style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);

    QPen linePen = painter->pen();
    linePen.setWidth(1);
    QColor lineColour = opt.palette.color(QPalette::Text);
    lineColour.setAlpha(40);
    linePen.setColor(lineColour);

    const auto title    = index.data(PlaylistItem::Role::Title).value<RichText>().blocks;
    const auto subtitle = index.data(PlaylistItem::Role::Subtitle).value<RichText>().blocks;

    const QRect rect            = subheaderContentRect(opt, index, 5);
    const int height            = rect.height();
    const int halfWidth         = rect.width() / 2;
    static constexpr int offset = 5;

    const QRect rightRect{rect.left() + halfWidth, rect.top(), halfWidth - offset, height};
    auto [rightBound, totalRightWidth]
        = drawTextBlocks(painter, opt, rightRect, subtitle | std::views::reverse, Qt::AlignVCenter | Qt::AlignRight);

    QRect leftRect{rect.left() + offset, rect.top(), rect.width() - totalRightWidth, height};
    if(totalRightWidth > 0) {
        leftRect.setWidth(leftRect.width() - (4 * offset));
    }
    auto [leftBound, _] = drawTextBlocks(painter, opt, leftRect, title, Qt::AlignVCenter | Qt::AlignLeft);

    if(title.empty()) {
        leftBound = {rect.left(), rect.top(), 0, height};
    }

    if(subtitle.empty()) {
        rightBound = {rect.right(), rect.top(), 0, height};
    }

    const int leftOffset  = !title.empty() ? 10 : offset;
    const int rightOffset = !subtitle.empty() ? 10 : offset;

    painter->setPen(linePen);
    const QLineF titleLine(leftBound.right() + leftOffset, leftBound.center().y() + 1, rightBound.left() - rightOffset,
                           rightBound.center().y() + 1);
    painter->drawLine(titleLine);
}

void paintTrack(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index)
{
    QStyleOptionViewItem opt{option};

    const QStyle* style = option.widget ? option.widget->style() : QApplication::style();

    const bool singleColumn = index.data(PlaylistItem::Role::SingleColumnMode).toBool();

    const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &opt, opt.widget) * 2;

    if(singleColumn) {
        QRect iconRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, opt.widget);
        QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt, opt.widget);
        textRect.adjust(textMargin, 0, -textMargin, 0);

        const int indent     = opt.icon.isNull() ? 0 : textMargin;
        const auto leftSide  = index.data(PlaylistItem::Role::Left).value<RichText>().blocks;
        const auto rightSide = index.data(PlaylistItem::Role::Right).value<RichText>().blocks;

        const QRect rightRect     = textRect.adjusted(textRect.center().x() - textRect.left(), 0, 0, 0);
        auto [_, totalRightWidth] = drawTextBlocks(painter, opt, rightRect, rightSide | std::views::reverse,
                                                   Qt::AlignVCenter | Qt::AlignRight);

        const QRect leftRect = textRect.adjusted(0, 0, -totalRightWidth, 0);
        drawTextBlocks(painter, opt, leftRect, leftSide, Qt::AlignVCenter | Qt::AlignLeft);

        if(!opt.icon.isNull()) {
            iconRect.moveLeft(iconRect.x() + indent);
            Gui::drawItemViewIcon(painter, opt, opt.icon, iconRect, Qt::AlignVCenter | Qt::AlignLeft);
        }
    }
    else {
        if(index.data(PlaylistItem::Role::Column).canConvert<QPixmap>()) {
            const auto image = index.data(PlaylistItem::Role::Column).value<QPixmap>();

            if(!image.isNull()) {
                const auto imagePadding    = index.data(PlaylistItem::Role::ImagePadding).toInt();
                const auto imagePaddingTop = index.data(PlaylistItem::Role::ImagePaddingTop).toInt();

                opt.rect.adjust(imagePadding, imagePaddingTop, -imagePadding, imagePaddingTop);

                style->drawItemPixmap(
                    painter, opt.rect, Qt::AlignHCenter | Qt::AlignTop,
                    Utils::scalePixmap(image, opt.rect.width(), opt.widget->devicePixelRatioF(), true));
            }
        }
        else {
            const auto decPos
                = index.data(PlaylistItem::Role::DecorationPosition).value<QStyleOptionViewItem::Position>();

            if(!opt.icon.isNull()) {
                opt.decorationPosition = decPos;
            }

            QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt, opt.widget);
            textRect.adjust(textMargin, 0, -textMargin, 0);

            const auto columnText = index.data(PlaylistItem::Role::Column).value<RichText>().blocks;

            const auto& [bound, totalWidth]
                = drawTextBlocks(painter, opt, textRect, columnText, Qt::AlignVCenter | opt.displayAlignment);

            if(!opt.icon.isNull()) {
                QRect iconRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, opt.widget);
                if(columnText.empty()) {
                    iconRect = QStyle::alignedRect(opt.direction, opt.displayAlignment, iconRect.size(), opt.rect);
                }
                else if(decPos == QStyleOptionViewItem::Right) {
                    iconRect = textRect.adjusted(totalWidth + textMargin, 0, totalWidth + textMargin, 0);
                }
                Gui::drawItemViewIcon(painter, opt, opt.icon, iconRect, opt.displayAlignment);
            }
        }
    }
}

void PlaylistDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
    if(auto* lineEdit = qobject_cast<QLineEdit*>(editor)) {
        const bool initialised = lineEdit->property(EditorInitialisedProperty).toBool();
        if(initialised && lineEdit->hasFocus()) {
            // Prevent track updates from updating the text in the editor
            return;
        }

        QStyledItemDelegate::setEditorData(editor, index);
        lineEdit->setProperty(EditorInitialisedProperty, true);
        return;
    }

    QStyledItemDelegate::setEditorData(editor, index);
}

void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    painter->save();

    QStyleOptionViewItem opt = option;
    initStyleOption(&opt, index);

    opt.showDecorationSelected = true;

    const auto type = index.data(PlaylistItem::Type).toInt();
    switch(type) {
        case PlaylistItem::Track:
            paintTrack(painter, opt, index);
            break;
        case PlaylistItem::Header: {
            const auto simple = index.data(PlaylistItem::Simple).toBool();
            simple ? paintSimpleHeader(painter, opt, index) : paintHeader(painter, opt, index);
            break;
        }
        case PlaylistItem::Subheader:
            paintSubheader(painter, opt, index);
            break;
        default:
            break;
    }

    painter->restore();
}

QSize PlaylistDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    QStyleOptionViewItem opt = option;
    initStyleOption(&opt, index);

    // Prevents the current playing row being larger than the rest
    opt.icon           = {};
    opt.decorationSize = {};

    const QWidget* widget = opt.widget;
    const QStyle* style   = widget ? widget->style() : QApplication::style();
    const int type        = index.data(PlaylistItem::Type).toInt();

    QSize size = index.data(Qt::SizeHintRole).toSize();
    const bool usesStyleBaseHeight
        = (type == PlaylistItem::Track && index.data(PlaylistItem::Role::UsesStyleBaseHeight).toBool());

    QSize hintSize{size};
    if(usesStyleBaseHeight) {
        hintSize.setHeight(0);
    }

    const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &opt, opt.widget) * 5;
    const QSize hint = style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, hintSize, widget);

    if(size.width() <= 0) {
        size.setWidth(hint.width());
    }
    if(usesStyleBaseHeight) {
        size.setHeight(hint.height() + size.height());
    }
    else if(size.height() < 1) {
        size.setHeight(hint.height());
    }

    size.setWidth(size.width() + margin);

    return size;
}
} // namespace Fooyin

#include "moc_playlistdelegate.cpp"
