#include "ItemIconCache.h"

#include <QFile>
#include <QFontMetrics>
#include <QPainter>
#include <QRectF>
#include <QSvgRenderer>

#include "VisualsConfig.h"

namespace
{

// How far the icon is inset within the item's colored square, as a fraction of the
// square's size on each side (REQ-UI-ITEM-ICON). The inset is what keeps a frame of the
// square's color visible all around the icon: each icon's viewBox is cropped tight to
// its artwork, so an icon drawn at the full rect would cover the square entirely.
const double kIconInsetFraction = 0.15;

}  // namespace


ItemIconCache::ItemIconCache(const QString& iconDir, const VisualsConfig* visuals)
    : m_iconDir(iconDir)
    , m_visuals(visuals)
{
}

const QByteArray& ItemIconCache::getSvg(const std::string& itemId)
{
    const std::map<std::string, QByteArray>::const_iterator cached =
        m_svgById.find(itemId);
    if (cached != m_svgById.end())
    {
        return cached->second;
    }

    // An absent or unreadable file caches an empty byte array so it is not retried;
    // a missing icon is not an error (REQ-UI-ITEM-ICON).
    QByteArray svg;
    QFile file(m_iconDir + "/" + QString::fromStdString(itemId) + ".svg");
    if (file.open(QIODevice::ReadOnly))
    {
        svg = file.readAll();
    }
    return m_svgById.emplace(itemId, std::move(svg)).first->second;
}

bool ItemIconCache::hasIcon(const std::string& itemId)
{
    return !getSvg(itemId).isEmpty();
}

void ItemIconCache::paintItem(QPainter& painter, const QRectF& rect,
                              const std::string& itemId)
{
    // The colored square from visuals.toml backs every item, icon or not: it is what
    // gives the item contrast against the tile beneath it in the world, and its outline
    // is what separates neighbouring items where they overlap on a belt (REQ-GW-TILE-SIZE,
    // REQ-UI-ITEM-ICON).
    if (m_visuals != nullptr)
    {
        const std::map<std::string, ItemVisuals>::const_iterator it =
            m_visuals->items.find(itemId);
        if (it != m_visuals->items.end())
        {
            painter.fillRect(rect, it->second.fill);
            painter.setPen(QPen(it->second.outline, 1));
            painter.setBrush(Qt::NoBrush);
            painter.drawRect(rect);
        }
    }

    if (!hasIcon(itemId)) { return; }

    // The icon goes on top, inset so a frame of the square's color stays visible all
    // around it. It is rasterized once at the inset pixel size and cached, so this is a
    // plain pixmap blit per frame.
    const double inset = kIconInsetFraction * rect.width();
    const QRectF iconRect = rect.adjusted(inset, inset, -inset, -inset);

    int sizePx = qRound(iconRect.width());
    if (sizePx < 1) { sizePx = 1; }
    painter.drawPixmap(iconRect, getPixmap(itemId, sizePx),
                       QRectF(0, 0, sizePx, sizePx));
}

QPixmap ItemIconCache::getSquarePixmap(const std::string& itemId, int sizePx)
{
    return getPixmap("square:" + itemId, itemId, sizePx, true);
}

QPixmap ItemIconCache::getPixmap(const std::string& itemId, int sizePx)
{
    return getPixmap(itemId, itemId, sizePx, false);
}

QPixmap ItemIconCache::getInlineIcon(const std::string& itemId, const QFont& font)
{
    if (!hasIcon(itemId)) { return QPixmap(); }
    return getPixmap(itemId, QFontMetrics(font).height());
}

QPixmap ItemIconCache::getPixmap(const std::string& cacheKey, const std::string& itemId,
                                 int sizePx, bool withSquare)
{
    if (sizePx <= 0)
    {
        return QPixmap();
    }

    const std::pair<std::string, int> key(cacheKey, sizePx);
    const std::map<std::pair<std::string, int>, QPixmap>::const_iterator cached =
        m_pixmapCache.find(key);
    if (cached != m_pixmapCache.end())
    {
        return cached->second;
    }

    QPixmap pixmap;
    if (withSquare)
    {
        // Null unless the item has something to draw -- a square, an icon, or both --
        // so a caller with nothing to show can fall back to text (REQ-UI-ITEM-ICON).
        const bool hasSquare = m_visuals != nullptr
            && m_visuals->items.find(itemId) != m_visuals->items.end();
        if (hasSquare || hasIcon(itemId))
        {
            pixmap = QPixmap(sizePx, sizePx);
            pixmap.fill(Qt::transparent);
            QPainter painter(&pixmap);
            // No antialiasing: the square's edges are axis-aligned and land on pixel
            // boundaries, and smoothing a 1-pixel outline only blurs it. The icon is
            // blitted into a slightly smaller rect than it was rasterized at, which is
            // what the smooth transform is for.
            painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
            // One pixel short of the pixmap so the square's right and bottom edges land
            // inside it rather than on its border.
            paintItem(painter, QRectF(0, 0, sizePx - 1, sizePx - 1), itemId);
        }
    }
    else
    {
        const QByteArray& svg = getSvg(itemId);
        if (!svg.isEmpty())
        {
            QSvgRenderer renderer(svg);
            pixmap = QPixmap(sizePx, sizePx);
            pixmap.fill(Qt::transparent);
            QPainter painter(&pixmap);
            painter.setRenderHint(QPainter::Antialiasing, true);
            renderer.render(&painter);
        }
    }
    return m_pixmapCache.emplace(key, std::move(pixmap)).first->second;
}

void ItemIconCache::clearPixmapCache()
{
    m_pixmapCache.clear();
}
