/***************************************************************************
 *   Copyright (c) 2004 Jürgen Riegel <juergen.riegel@web.de>              *
 *                                                                         *
 *   This file is part of the FreeCAD CAx development system.              *
 *                                                                         *
 *   This library is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU Library General Public           *
 *   License as published by the Free Software Foundation; either          *
 *   version 2 of the License, or (at your option) any later version.      *
 *                                                                         *
 *   This library  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 Library General Public License for more details.                  *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this library; see the file COPYING.LIB. If not,    *
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
 *   Suite 330, Boston, MA  02111-1307, USA                                *
 *                                                                         *
 ***************************************************************************/

#include <QApplication>
#include <QBitmap>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMap>
#include <QImageReader>
#include <QPainter>
#include <QPalette>
#include <QScreen>
#include <QString>
#include <QSvgRenderer>
#include <QStyleOption>

#include <string>
#include <Inventor/fields/SoSFImage.h>

#include <App/Application.h>
#include <Base/Console.h>
#include <Base/ConsoleObserver.h>

#include "BitmapFactory.h"

using namespace Gui;

namespace Gui
{
class BitmapFactoryInstP
{
public:
    QMap<std::string, QPixmap> xpmCache;

    bool useIconTheme;
};
}  // namespace Gui

BitmapFactoryInst* BitmapFactoryInst::_pcSingleton = nullptr;

BitmapFactoryInst& BitmapFactoryInst::instance()
{
    if (!_pcSingleton) {
        _pcSingleton = new BitmapFactoryInst;
        std::map<std::string, std::string>::const_iterator it;
        it = App::GetApplication().Config().find("ProgramIcons");
        if (it != App::GetApplication().Config().end()) {
            QString home = QString::fromStdString(App::Application::getHomePath());
            QString path = QString::fromUtf8(it->second.c_str());
            if (QDir(path).isRelative()) {
                path = QFileInfo(QDir(home), path).absoluteFilePath();
            }
            _pcSingleton->addPath(path);
        }
        _pcSingleton->addPath(
            QStringLiteral("%1/icons").arg(QString::fromStdString(App::Application::getHomePath()))
        );
        _pcSingleton->addPath(
            QStringLiteral("%1/icons").arg(QString::fromStdString(App::Application::getUserAppDataDir()))
        );
        _pcSingleton->addPath(QLatin1String(":/icons/"));
        _pcSingleton->addPath(QLatin1String(":/Icons/"));
    }

    return *_pcSingleton;
}

void BitmapFactoryInst::destruct()
{
    if (_pcSingleton) {
        delete _pcSingleton;
    }
    _pcSingleton = nullptr;
}

BitmapFactoryInst::BitmapFactoryInst()
{
    d = new BitmapFactoryInstP;

    restoreCustomPaths();
    configureUseIconTheme();
}

BitmapFactoryInst::~BitmapFactoryInst()
{
    delete d;
}

void BitmapFactoryInst::restoreCustomPaths()
{
    Base::Reference<ParameterGrp> group = App::GetApplication().GetParameterGroupByPath(
        "User parameter:BaseApp/Preferences/Bitmaps"
    );
    std::vector<std::string> paths = group->GetASCIIs("CustomPath");
    for (auto& path : paths) {
        addPath(QString::fromUtf8(path.c_str()));
    }
}

void Gui::BitmapFactoryInst::configureUseIconTheme()
{
    Base::Reference<ParameterGrp> group = App::GetApplication().GetParameterGroupByPath(
        "User parameter:BaseApp/Preferences/Bitmaps/Theme"
    );

    d->useIconTheme = group->GetBool("UseIconTheme", group->GetBool("ThemeSearchPaths", false));
}

void BitmapFactoryInst::addPath(const QString& path)
{
    QDir::addSearchPath(QStringLiteral("icons"), path);
}

void BitmapFactoryInst::removePath(const QString& path)
{
    QStringList iconPaths = QDir::searchPaths(QStringLiteral("icons"));
    int pos = iconPaths.indexOf(path);
    if (pos != -1) {
        iconPaths.removeAt(pos);
        QDir::setSearchPaths(QStringLiteral("icons"), iconPaths);
    }
}

QStringList BitmapFactoryInst::getPaths() const
{
    return QDir::searchPaths(QStringLiteral("icons"));
}

QStringList BitmapFactoryInst::findIconFiles() const
{
    QStringList files, filters;
    QList<QByteArray> formats = QImageReader::supportedImageFormats();
    for (QList<QByteArray>::iterator it = formats.begin(); it != formats.end(); ++it) {
        filters << QStringLiteral("*.%1").arg(QString::fromLatin1(*it).toLower());
    }

    QStringList paths = QDir::searchPaths(QStringLiteral("icons"));
    paths.removeDuplicates();
    for (QStringList::Iterator pt = paths.begin(); pt != paths.end(); ++pt) {
        QDir d(*pt);
        d.setNameFilters(filters);
        QFileInfoList fi = d.entryInfoList();
        for (QFileInfoList::iterator it = fi.begin(); it != fi.end(); ++it) {
            files << it->absoluteFilePath();
        }
    }

    files.removeDuplicates();
    return files;
}

void BitmapFactoryInst::addPixmapToCache(const char* name, const QPixmap& icon)
{
    d->xpmCache[name] = icon;
}

bool BitmapFactoryInst::findPixmapInCache(const char* name, QPixmap& px) const
{
    QMap<std::string, QPixmap>::Iterator it = d->xpmCache.find(name);
    if (it != d->xpmCache.end()) {
        px = it.value();
        return true;
    }
    return false;
}

QIcon BitmapFactoryInst::iconFromTheme(const char* name, const QIcon& fallback)
{
    if (!d->useIconTheme) {
        return iconFromDefaultTheme(name, fallback);
    }

    QString iconName = QString::fromUtf8(name);
    QIcon icon = QIcon::fromTheme(iconName, fallback);
    if (icon.isNull()) {
        QPixmap px = pixmap(name);
        if (!px.isNull()) {
            icon.addPixmap(px);
        }
    }

    return icon;
}

bool BitmapFactoryInst::loadPixmap(const QString& filename, QPixmap& icon) const
{
    QFileInfo fi(filename);
    if (fi.exists()) {
        // first check if it's an SVG because Qt's qsvg4 module shouldn't be used therefore
        if (fi.suffix().toLower() == QLatin1String("svg")) {
            QFile svgFile(fi.filePath());
            if (svgFile.open(QFile::ReadOnly | QFile::Text)) {
                QByteArray content = svgFile.readAll();
                icon = pixmapFromSvg(content, QSize(64, 64));
            }
        }
        else {
            // try with Qt plugins
            icon.load(fi.filePath());
        }
    }

    return !icon.isNull();
}

QIcon Gui::BitmapFactoryInst::iconFromDefaultTheme(const char* name, const QIcon& fallback)
{
    QIcon icon;
    QPixmap px = pixmap(name);

    if (!px.isNull()) {
        icon.addPixmap(px);
        return icon;
    }
    else {
        return fallback;
    }

    return icon;
}

QPixmap BitmapFactoryInst::pixmap(const char* name) const
{
    if (!name || *name == '\0') {
        return {};
    }

    // as very first test check whether the pixmap is in the cache
    QMap<std::string, QPixmap>::Iterator it = d->xpmCache.find(name);
    if (it != d->xpmCache.end()) {
        return it.value();
    }

    QPixmap icon;

    // Try whether an absolute path is given
    QString fn = QString::fromUtf8(name);
    loadPixmap(fn, icon);

    // try to find it in the 'icons' search paths
    if (icon.isNull()) {
        QList<QByteArray> formats = QImageReader::supportedImageFormats();
        formats.prepend("SVG");  // check first for SVG to use special import mechanism

        QString fileName = QStringLiteral("icons:") + fn;
        if (!loadPixmap(fileName, icon)) {
            // Go through supported file formats
            for (QList<QByteArray>::iterator fm = formats.begin(); fm != formats.end(); ++fm) {
                QString path = QStringLiteral("%1.%2").arg(
                    fileName,
                    QString::fromLatin1((*fm).toLower().constData())
                );
                if (loadPixmap(path, icon)) {
                    break;
                }
            }
        }
    }

    if (!icon.isNull()) {
        d->xpmCache[name] = icon;
        return icon;
    }

    Base::Console().warning("Cannot find icon: %s\n", name);
    return QPixmap(Gui::BitmapFactory().pixmapFromSvg("help-browser", QSize(16, 16)));
}

QPixmap BitmapFactoryInst::pixmapFromSvg(
    const char* name,
    const QSizeF& size,
    const ColorMap& colorMapping
) const
{
    static qreal dpr = getMaximumDPR();

    // If an absolute path is given
    QPixmap icon;
    QString iconPath;
    QString fn = QString::fromUtf8(name);
    if (QFile(fn).exists()) {
        iconPath = fn;
    }

    // try to find it in the 'icons' search paths
    if (iconPath.isEmpty()) {
        QString fileName = QStringLiteral("icons:") + fn;
        QFileInfo fi(fileName);
        if (fi.exists()) {
            iconPath = fi.filePath();
        }
        else {
            fileName += QLatin1String(".svg");
            fi.setFile(fileName);
            if (fi.exists()) {
                iconPath = fi.filePath();
            }
        }
    }

    if (!iconPath.isEmpty()) {
        QFile file(iconPath);
        if (file.open(QFile::ReadOnly | QFile::Text)) {
            QByteArray content = file.readAll();
            icon = pixmapFromSvg(content, size * dpr, colorMapping);
        }
    }

    if (!icon.isNull()) {
        icon.setDevicePixelRatio(dpr);
    }

    return icon;
}

QPixmap BitmapFactoryInst::pixmapFromSvg(
    const QByteArray& originalContents,
    const QSizeF& size,
    const ColorMap& colorMapping
) const
{
    QString stringContents = QString::fromUtf8(originalContents);
    for (const auto& colorToColor : colorMapping) {
        ulong fromColor = colorToColor.first;
        ulong toColor = colorToColor.second;
        QString fromColorString = QStringLiteral("#%1").arg(fromColor, 6, 16, QChar::fromLatin1('0'));
        QString toColorString = QStringLiteral("#%1").arg(toColor, 6, 16, QChar::fromLatin1('0'));
        stringContents = stringContents.replace(fromColorString, toColorString);
    }
    QByteArray contents = stringContents.toUtf8();

    QImage image(size.toSize(), QImage::Format_ARGB32_Premultiplied);
    image.fill(0x00000000);

    QPainter p(&image);
    QSvgRenderer svg;
    {
        // tmp. disable the report window to suppress some bothering warnings
        const Base::ILoggerBlocker blocker("ReportOutput", Base::ConsoleSingleton::MsgType_Wrn);
        svg.load(contents);
    }
    svg.render(&p);
    p.end();

    return QPixmap::fromImage(image);
}

QStringList BitmapFactoryInst::pixmapNames() const
{
    QStringList names;
    for (QMap<std::string, QPixmap>::Iterator It = d->xpmCache.begin(); It != d->xpmCache.end();
         ++It) {
        QString item = QString::fromUtf8(It.key().c_str());
        if (!names.contains(item)) {
            names << item;
        }
    }
    return names;
}

QPixmap BitmapFactoryInst::resize(int w, int h, const QPixmap& p, Qt::BGMode bgmode) const
{
    if (bgmode == Qt::TransparentMode) {
        if (p.width() == 0 || p.height() == 0) {
            w = 1;
        }

        QPixmap pix = p;
        int x = pix.width() > w ? 0 : (w - pix.width()) / 2;
        int y = pix.height() > h ? 0 : (h - pix.height()) / 2;

        if (x == 0 && y == 0) {
            return pix;
        }

        QPixmap pm(w, h);
        QBitmap mask(w, h);
        mask.fill(Qt::color0);

        QBitmap bm = pix.mask();
        if (!bm.isNull()) {
            QPainter painter(&mask);
            painter.drawPixmap(QPoint(x, y), bm, QRect(0, 0, pix.width(), pix.height()));
            pm.setMask(mask);
        }
        else {
            pm.setMask(mask);
            pm = fillRect(x, y, pix.width(), pix.height(), pm, Qt::OpaqueMode);
        }

        QPainter pt;
        pt.begin(&pm);
        pt.drawPixmap(x, y, pix);
        pt.end();
        return pm;
    }
    else {  // Qt::OpaqueMode
        QPixmap pix = p;

        if (pix.width() == 0 || pix.height() == 0) {
            return pix;  // do not resize a null pixmap
        }

        QPalette pal = qApp->palette();
        QColor dl = pal.color(QPalette::Disabled, QPalette::Light);
        QColor dt = pal.color(QPalette::Disabled, QPalette::Text);

        QPixmap pm(w, h);
        pm.fill(dl);

        QPainter pt;
        pt.begin(&pm);
        pt.setPen(dl);
        pt.drawPixmap(1, 1, pix);
        pt.setPen(dt);
        pt.drawPixmap(0, 0, pix);
        pt.end();
        return pm;
    }
}

QPixmap BitmapFactoryInst::fillRect(int x, int y, int w, int h, const QPixmap& p, Qt::BGMode bgmode) const
{
    QBitmap b = p.mask();
    if (b.isNull()) {
        return p;  // sorry, but cannot do anything
    }

    QPixmap pix = p;

    // modify the mask
    QPainter pt;
    pt.begin(&b);
    if (bgmode == Qt::OpaqueMode) {
        pt.fillRect(x, y, w, h, Qt::color1);  // make opaque
    }
    else {                                    // Qt::TransparentMode
        pt.fillRect(x, y, w, h, Qt::color0);  // make transparent
    }
    pt.end();

    pix.setMask(b);

    return pix;
}

QPixmap BitmapFactoryInst::merge(const QPixmap& p1, const QPixmap& p2, bool vertical) const
{
    int width = 0;
    int height = 0;

    int x = 0;
    int y = 0;

    // get the size for the new pixmap
    if (vertical) {
        y = p1.height();
        width = qMax(p1.width(), p2.width());
        height = p1.height() + p2.height();
    }
    else {
        x = p1.width();
        width = p1.width() + p2.width();
        height = qMax(p1.height(), p2.height());
    }

    QPixmap res(width, height);
    QBitmap mask(width, height);
    QBitmap mask1 = p1.mask();
    QBitmap mask2 = p2.mask();
    mask.fill(Qt::color0);

    auto* pt1 = new QPainter(&res);
    pt1->drawPixmap(0, 0, p1);
    pt1->drawPixmap(x, y, p2);
    delete pt1;

    auto* pt2 = new QPainter(&mask);
    pt2->drawPixmap(0, 0, mask1);
    pt2->drawPixmap(x, y, mask2);
    delete pt2;

    res.setMask(mask);
    return res;
}

QPixmap BitmapFactoryInst::merge(const QPixmap& p1, const QPixmap& p2, Position pos) const
{
    // does the similar as the method above except that this method does not resize the resulting pixmap
    int x = 0, y = 0;
    qreal dpr1 = p1.devicePixelRatio();
    qreal dpr2 = p2.devicePixelRatio();

    switch (pos) {
        case TopLeft:
            break;
        case TopRight:
            x = p1.width() / dpr1 - p2.width() / dpr2;
            break;
        case BottomLeft:
            y = p1.height() / dpr1 - p2.height() / dpr2;
            break;
        case BottomRight:
            x = p1.width() / dpr1 - p2.width() / dpr2;
            y = p1.height() / dpr1 - p2.height() / dpr2;
            break;
    }

    QPixmap p = p1;
    p = fillRect(x, y, p2.width(), p2.height(), p, Qt::OpaqueMode);

    QPainter pt;
    pt.begin(&p);
    pt.setPen(Qt::NoPen);
    pt.drawRect(x, y, p2.width(), p2.height());
    pt.drawPixmap(x, y, p2);
    pt.end();

    return p;
}

QPixmap BitmapFactoryInst::disabled(const QPixmap& p) const
{
    QStyleOption opt;
    opt.palette = QApplication::palette();
    return QApplication::style()->generatedIconPixmap(QIcon::Disabled, p, &opt);
}

QPixmap BitmapFactoryInst::empty(QSize size) const
{
    qreal dpr = getMaximumDPR();

    QPixmap res(size * dpr);
    res.fill(Qt::transparent);
    res.setDevicePixelRatio(dpr);

    return res;
}

void BitmapFactoryInst::convert(const QImage& p, SoSFImage& img) const
{
    SbVec2s size;
    size[0] = p.width();
    size[1] = p.height();

    int buffersize = static_cast<int>(p.sizeInBytes());

    int numcomponents = 0;
    QVector<QRgb> table = p.colorTable();
    if (!table.isEmpty()) {
        if (p.hasAlphaChannel()) {
            if (p.allGray()) {
                numcomponents = 2;
            }
            else {
                numcomponents = 4;
            }
        }
        else {
            if (p.allGray()) {
                numcomponents = 1;
            }
            else {
                numcomponents = 3;
            }
        }
    }
    else {
        numcomponents = buffersize / (size[0] * size[1]);
    }

    int depth = numcomponents;

    // Coin3D only supports up to 32-bit images
    if (numcomponents == 8) {
        numcomponents = 4;
    }

    // allocate image data
    img.setValue(size, numcomponents, nullptr);

    unsigned char* bytes = img.startEditing(size, numcomponents);

    int width = (int)size[0];
    int height = (int)size[1];

    for (int y = 0; y < height; y++) {
        unsigned char* line = &bytes[width * numcomponents * (height - (y + 1))];
        for (int x = 0; x < width; x++) {
            QColor col = p.pixelColor(x, y);
            switch (depth) {
                default:
                    break;
                case 1: {
                    QRgb rgb = col.rgb();
                    line[0] = qGray(rgb);
                } break;
                case 2: {
                    QRgb rgb = col.rgba();
                    line[0] = qGray(rgb);
                    line[1] = qAlpha(rgb);
                } break;
                case 3: {
                    QRgb rgb = col.rgb();
                    line[0] = qRed(rgb);
                    line[1] = qGreen(rgb);
                    line[2] = qBlue(rgb);
                } break;
                case 4: {
                    QRgb rgb = col.rgba();
                    line[0] = qRed(rgb);
                    line[1] = qGreen(rgb);
                    line[2] = qBlue(rgb);
                    line[3] = qAlpha(rgb);
                } break;
                case 8: {
                    QRgba64 rgb = col.rgba64();
                    line[0] = qRed(rgb);
                    line[1] = qGreen(rgb);
                    line[2] = qBlue(rgb);
                    line[3] = qAlpha(rgb);
                } break;
            }

            line += numcomponents;
        }
    }

    img.finishEditing();
}

void BitmapFactoryInst::convert(const SoSFImage& p, QImage& img) const
{
    SbVec2s size;
    int numcomponents;

    const unsigned char* bytes = p.getValue(size, numcomponents);
    if (!bytes) {
        return;
    }

    int width = (int)size[0];
    int height = (int)size[1];

    img = QImage(width, height, QImage::Format_RGB32);
    QRgb* bits = (QRgb*)img.bits();

    for (int y = 0; y < height; y++) {
        const unsigned char* line = &bytes[width * numcomponents * (height - (y + 1))];
        for (int x = 0; x < width; x++) {
            switch (numcomponents) {
                default:
                case 1:
                    *bits++ = qRgb(line[0], line[0], line[0]);
                    break;
                case 2:
                    *bits++ = qRgba(line[0], line[0], line[0], line[1]);
                    break;
                case 3:
                    *bits++ = qRgb(line[0], line[1], line[2]);
                    break;
                case 4:
                    *bits++ = qRgba(line[0], line[1], line[2], line[3]);
                    break;
            }

            line += numcomponents;
        }
    }
}

QIcon BitmapFactoryInst::mergePixmap(
    const QIcon& base,
    const QPixmap& px,
    Gui::BitmapFactoryInst::Position position
)
{
    QIcon overlayedIcon;

    int w = QApplication::style()->pixelMetric(QStyle::PM_ListViewIconSize);

    overlayedIcon.addPixmap(
        Gui::BitmapFactory().merge(base.pixmap(w, w, QIcon::Normal, QIcon::Off), px, position),
        QIcon::Normal,
        QIcon::Off
    );

    overlayedIcon.addPixmap(
        Gui::BitmapFactory().merge(base.pixmap(w, w, QIcon::Normal, QIcon::On), px, position),
        QIcon::Normal,
        QIcon::Off
    );

    return overlayedIcon;
}

qreal BitmapFactoryInst::getMaximumDPR()
{
    qreal dpr = 1.0F;

    for (QScreen* screen : QGuiApplication::screens()) {
        dpr = std::max(screen->devicePixelRatio(), dpr);
    }

    return dpr;
}
