/***************************************************************************
 *   Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net>     *
 *                                                                         *
 *   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 <cstdlib>
#include <QApplication>
#include <QClipboard>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMutex>
#include <QPainter>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QWaitCondition>


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

#include "BitmapFactory.h"
#include "SplashScreen.h"
#include "Tools.h"

using namespace Gui;

namespace Gui
{

/** Displays all messages at startup inside the splash screen.
 * \author Werner Mayer
 */
class SplashObserver: public Base::ILogger
{
public:
    SplashObserver(const SplashObserver&) = delete;
    SplashObserver(SplashObserver&&) = delete;
    SplashObserver& operator=(const SplashObserver&) = delete;
    SplashObserver& operator=(SplashObserver&&) = delete;

    explicit SplashObserver(QSplashScreen* splasher = nullptr)
        : splash(splasher)
        , alignment(Qt::AlignBottom | Qt::AlignLeft)
        , textColor(Qt::black)
    {
        Base::Console().attachObserver(this);

        // allow to customize text position and color
        const std::map<std::string, std::string>& cfg = App::Application::Config();
        auto al = cfg.find("SplashAlignment");
        if (al != cfg.end()) {
            QString alt = QString::fromLatin1(al->second.c_str());
            int align = 0;
            if (alt.startsWith(QLatin1String("VCenter"))) {
                align = Qt::AlignVCenter;
            }
            else if (alt.startsWith(QLatin1String("Top"))) {
                align = Qt::AlignTop;
            }
            else {
                align = Qt::AlignBottom;
            }

            if (alt.endsWith(QLatin1String("HCenter"))) {
                align += Qt::AlignHCenter;
            }
            else if (alt.endsWith(QLatin1String("Right"))) {
                align += Qt::AlignRight;
            }
            else {
                align += Qt::AlignLeft;
            }

            alignment = align;
        }

        // choose text color
        auto tc = cfg.find("SplashTextColor");
        if (tc != cfg.end()) {
            QColor col(QString::fromStdString(tc->second));
            if (col.isValid()) {
                textColor = col;
            }
        }
    }
    ~SplashObserver() override
    {
        Base::Console().detachObserver(this);
    }
    const char* name() override
    {
        return "SplashObserver";
    }
    void sendLog(
        const std::string& notifiername,
        const std::string& msg,
        Base::LogStyle level,
        Base::IntendedRecipient recipient,
        Base::ContentType content
    ) override
    {
        Q_UNUSED(notifiername)
        Q_UNUSED(recipient)
        Q_UNUSED(content)

#ifdef FC_DEBUG
        Q_UNUSED(level)
        Log(msg);
#else
        if (level == Base::LogStyle::Log) {
            Log(msg);
        }
#endif
    }
    void Log(const std::string& text)
    {
        QString msg(QString::fromStdString(text));
        QRegularExpression rx;
        // ignore 'Init:' and 'Mod:' prefixes
        rx.setPattern(QLatin1String("^\\s*(Init:|Mod:)\\s*"));
        auto match = rx.match(msg);
        if (match.hasMatch()) {
            msg = msg.mid(match.capturedLength());
        }
        else {
            // ignore activation of commands
            rx.setPattern(QLatin1String(R"(^\s*(\+App::|Create|CmdC:|CmdG:|Act:)\s*)"));
            match = rx.match(msg);
            if (match.hasMatch() && match.capturedStart() == 0) {
                return;
            }
        }

        splash->showMessage(msg.replace(QLatin1String("\n"), QString()), alignment, textColor);
        QMutex mutex;
        QMutexLocker ml(&mutex);
        QWaitCondition().wait(&mutex, 50);
    }

private:
    QSplashScreen* splash;
    int alignment;
    QColor textColor;
};

/**
 * Displays a warning about this being a developer build. Designed for display in the Splashscreen.
 * \param painter The painter to draw the warning into
 * \param startPosition The painter-space coordinates to start the warning box at.
 * \param maxSize The maximum extents for the box that is drawn. If the text exceeds this size it
 * will be scaled down to fit.
 * \note The text string is translatable, so its length is somewhat unpredictable. It is always
 * displayed as two lines, regardless of the length of the text (e.g. no wrapping is done). Only the
 * width is considered, the height simply follows from the font size.
 */
static void renderDevBuildWarning(
    QPainter& painter,
    const QPoint startPosition,
    const QSize maxSize,
    QColor color
)
{
    // Create a background box that fades out the artwork for better legibility
    QColor fader(Qt::white);
    constexpr float halfDensity(0.65F);
    fader.setAlphaF(halfDensity);
    QBrush fillBrush(fader, Qt::BrushStyle::SolidPattern);
    painter.setBrush(fillBrush);

    // Construct the lines of text and figure out how much space they need
    const auto devWarningLine1 = QObject::tr("WARNING: This is a development version.");
    const auto devWarningLine2 = QObject::tr("Do not use it in a production environment.");
    QFontMetrics fontMetrics(painter.font());  // Try to use the existing font
    int padding = QtTools::horizontalAdvance(fontMetrics, QLatin1String("M"));  // Arbitrary
    int line1Width = QtTools::horizontalAdvance(fontMetrics, devWarningLine1);
    int line2Width = QtTools::horizontalAdvance(fontMetrics, devWarningLine2);
    int boxWidth = std::max(line1Width, line2Width) + 2 * padding;
    int lineHeight = fontMetrics.lineSpacing();
    if (boxWidth > maxSize.width()) {
        // Especially if the text was translated, there is a chance that using the existing font
        // will exceed the width of the Splashscreen graphic. Resize down so that it fits, no matter
        // how long the text strings are.
        float reductionFactor = static_cast<float>(maxSize.width()) / static_cast<float>(boxWidth);
        int newFontSize = static_cast<int>(painter.font().pointSize() * reductionFactor);
        padding *= reductionFactor;
        QFont newFont = painter.font();
        newFont.setPointSize(newFontSize);
        painter.setFont(newFont);
        lineHeight = painter.fontMetrics().lineSpacing();
        boxWidth = maxSize.width();
    }
    constexpr float lineExpansionFactor(2.3F);
    int boxHeight = static_cast<int>(lineHeight * lineExpansionFactor);

    // Draw the background rectangle and the text
    painter.setPen(color);
    painter.drawRect(startPosition.x(), startPosition.y(), boxWidth, boxHeight);
    painter.drawText(startPosition.x() + padding, startPosition.y() + lineHeight, devWarningLine1);
    painter.drawText(startPosition.x() + padding, startPosition.y() + 2 * lineHeight, devWarningLine2);
}

}  // namespace Gui

// ------------------------------------------------------------------------------

/**
 * Constructs a splash screen that will display the pixmap.
 */
SplashScreen::SplashScreen(const QPixmap& pixmap, Qt::WindowFlags f)
    : QSplashScreen(pixmap, f)
{
    // write the messages to splasher
    messages = new SplashObserver(this);
}

/** Destruction. */
SplashScreen::~SplashScreen()
{
    delete messages;
}

/**
 * Draws the contents of the splash screen using painter \a painter. The default
 * implementation draws the message passed by message().
 */
void SplashScreen::drawContents(QPainter* painter)
{
    QSplashScreen::drawContents(painter);
}

void SplashScreen::setShowMessages(bool on)
{
    messages->bErr = on;
    messages->bMsg = on;
    messages->bLog = on;
    messages->bWrn = on;
}

QPixmap SplashScreen::splashImage()
{
    // search in the UserAppData dir as very first
    QPixmap splash_image;
    QFileInfo fi(QStringLiteral("images:splash_image.png"));
    if (fi.isFile() && fi.exists()) {
        splash_image.load(fi.filePath(), "PNG");
    }

    // if no image was found try the config
    std::string splash_path = App::Application::Config()["SplashScreen"];
    if (splash_image.isNull()) {
        QString path = QString::fromStdString(splash_path);
        if (QDir(path).isRelative()) {
            QString home = QString::fromStdString(App::Application::getHomePath());
            path = QFileInfo(QDir(home), path).absoluteFilePath();
        }

        splash_image.load(path);
    }

    // now try the icon paths
    float pixelRatio(1.0);
    if (splash_image.isNull()) {
        // determine the count of splashes
        QStringList pixmaps = Gui::BitmapFactory().findIconFiles().filter(
            QString::fromStdString(splash_path)
        );
        // divide by 2 since there's two sets (normal and 2x)
        // minus 1 to ignore the default splash that isn't numbered
        int splash_count = pixmaps.count() / 2 - 1;

        // set a random splash path
        if (splash_count > 0) {
            int random = rand() % splash_count;
            splash_path += std::to_string(random);
        }

        if (qApp->devicePixelRatio() > 1.0) {
            // For HiDPI screens, we have a double-resolution version of the splash image
            splash_path += "_2x";
            splash_image = Gui::BitmapFactory().pixmap(splash_path.c_str());
            splash_image.setDevicePixelRatio(2.0);
            pixelRatio = 2.0;
        }
        else {
            splash_image = Gui::BitmapFactory().pixmap(splash_path.c_str());
        }
    }

    // include application name and version number
    std::map<std::string, std::string>::const_iterator tc = App::Application::Config().find(
        "SplashInfoColor"
    );
    std::map<std::string, std::string>::const_iterator wc = App::Application::Config().find(
        "SplashWarningColor"
    );
    if (tc != App::Application::Config().end() && wc != App::Application::Config().end()) {
        QString title = qApp->applicationName();
        QString major = QString::fromStdString(App::Application::Config()["BuildVersionMajor"]);
        QString minor = QString::fromStdString(App::Application::Config()["BuildVersionMinor"]);
        QString point = QString::fromStdString(App::Application::Config()["BuildVersionPoint"]);
        QString suffix = QString::fromStdString(App::Application::Config()["BuildVersionSuffix"]);
        QString version = QStringLiteral("%1.%2.%3%4").arg(major, minor, point, suffix);
        QString position, fontFamily;

        std::map<std::string, std::string>::const_iterator te = App::Application::Config().find(
            "SplashInfoExeName"
        );
        std::map<std::string, std::string>::const_iterator tv = App::Application::Config().find(
            "SplashInfoVersion"
        );
        std::map<std::string, std::string>::const_iterator tp = App::Application::Config().find(
            "SplashInfoPosition"
        );
        std::map<std::string, std::string>::const_iterator tf = App::Application::Config().find(
            "SplashInfoFont"
        );
        if (te != App::Application::Config().end()) {
            title = QString::fromStdString(te->second);
        }
        if (tv != App::Application::Config().end()) {
            version = QString::fromStdString(tv->second);
        }
        if (tp != App::Application::Config().end()) {
            position = QString::fromStdString(tp->second);
        }
        if (tf != App::Application::Config().end()) {
            fontFamily = QString::fromStdString(tf->second);
        }

        QPainter painter;
        painter.begin(&splash_image);
        if (!fontFamily.isEmpty()) {
            QFont font = painter.font();
            if (font.fromString(fontFamily)) {
                painter.setFont(font);
            }
        }

        QFont fontExe = painter.font();
        fontExe.setPointSizeF(20.0);
        QFontMetrics metricExe(fontExe);
        int l = QtTools::horizontalAdvance(metricExe, title);
        if (title == QLatin1String("FreeCAD")) {
            l = 0.0;  // "FreeCAD" text is already part of the splashscreen, version goes below it
        }
        int w = splash_image.width();
        int h = splash_image.height();

        QFont fontVer = painter.font();
        fontVer.setPointSizeF(11.0);
        QFontMetrics metricVer(fontVer);
        int v = QtTools::horizontalAdvance(metricVer, version);

        int x = -1, y = -1;
        QRegularExpression rx(QLatin1String("(\\d+).(\\d+)"));
        auto match = rx.match(position);
        if (match.hasMatch()) {
            x = match.captured(1).toInt();
            y = match.captured(2).toInt();
        }
        else {
            x = w - (l + v + 10);
            y = h - 20;
        }

        QColor color(QString::fromStdString(tc->second));
        if (color.isValid()) {
            painter.setPen(color);
            painter.setFont(fontExe);
            if (title != QLatin1String("FreeCAD")) {
                // FreeCAD's Splashscreen already contains the EXE name, no need to draw it
                painter.drawText(x, y, title);
            }
            painter.setFont(fontVer);
            painter.drawText(x + (l + 235), y - 7, version);
            QColor warningColor(QString::fromStdString(wc->second));
            if (suffix == QLatin1String("dev") && warningColor.isValid()) {
                fontVer.setPointSizeF(14.0);
                painter.setFont(fontVer);
                const int lineHeight = metricVer.lineSpacing();
                const int padding {45};  // Distance from the edge of the graphic's bounding box
                QPoint startPosition(padding, y + lineHeight * 2);
                QSize maxSize(w / pixelRatio - 2 * padding, lineHeight * 3);
                renderDevBuildWarning(painter, startPosition, maxSize, warningColor);
            }
            painter.end();
        }
    }

    return splash_image;
}
